Saturday, 10 October 2009

Hi all

So, the other day I had this requirement for a BizTalk pipeline component:

Take an InfoPath formula and convert it into a PDF that is to be sent out via email. This seemed easy enough. I searched a bit, and found that three simple steps were needed:

  1. Install this: 2007 Microsoft Office Add-in: Microsoft Save as PDF 
  2. In my code, reference Microsoft.Office.InfoPath.dll and Microsoft.Office.InfoPath.FormControl.dll
  3. Write these lines of code:
   1: FormControl formControl = new FormControl();
   2: formControl.Open(pInMsg.Data);
   3: string output = Path.GetTempFileName();
   4: formControl.XmlForm.CurrentView.Export(output, Microsoft.Office.InfoPath.ExportFormat.Pdf);

Of course, this would also mean some code that would read the pdf file back in and then create the output message. But hey, that was just the price I had to pay.

BUT… I was being naive… As the more clever of my readers have probably all ready realized, if something is called FORMcontrol, then it is for programs that have a UI. The code crashed big time at runtime with some ActiveX exception :-(

Then I remembered that I have a colleague who had previously told me that she had done this at some point, so I emailed her for her code.

Unfortunately, her code involved taking the form, extracting the XSL from the XSN file, perform a transformation on the XML using the XSL which will generate HTML and then using some utility to convert this into PDF. This was more complex than I had hoped, but I saw no other way. Unfortunately, her code had this line in it:

   1: StreamReader stream = new StreamReader(XmlFormView.XmlForm.Template.OpenFileFromPackage("View1.xsl"));
which, as you might have guessed also requires a UI, in this case it is used in a web application. So no go.

So, it seems that I will have to do a lot of dirty work myself :-(

This turned into quite a list of subtasks:

  • Take the XML document that comes through the pipeline component
  • Take the value of the processing instruction called “mso-infoPathSolution” This processing instruction is always present in an InfoPath form and it looks something like this:
    <?mso-infoPathSolution solutionVersion="1.0.0.2" productVersion="12.0.0" PIVersion="1.0.0.0" href="http://path.to/form.xsn" name="urn:schemas-microsoft-com:office:infopath:MyForm:-myXSD-2009-09-21T15-43-10" ?>
  • Take the value of the href “attribute” that is in the value of the processing instruction. The href is a URI that points to the XSN that this XML is an instance of, you see.
  • Get the XSN file that is located at the URI.
  • Extract the XSL file that matches the view of the form you want to convert into PDF.
  • Perform the transformation
  • Convert into PDF

 

So I am now going from the few lines of code I was hoping for to a more complex solution… so lets look at the code:

First of all, I need the value of the processing instruction. This is easily done:

   1: private static string GetHrefFromXml(XmlDocument infoPathForm)
   2: {
   3:     XmlNode piNode = infoPathForm.SelectSingleNode("/processing-instruction(\"mso-infoPathSolution\")");
   4:     if (piNode != null && piNode is XmlProcessingInstruction)
   5:     {
   6:         var pi = (XmlProcessingInstruction)piNode;
   7:         string href = pi.Value;
   8:         int location = href.IndexOf(Href);
   9:         if (location != -1)
  10:         {
  11:             href = href.Substring(location + Href.Length);
  12:             href = href.Substring(0, href.IndexOf("\""));
  13:             return href;
  14:         }
  15:         throw new ApplicationException("No href attribute was found in the procesing instruction (mso-infoPathSolution). Without this, the location of the form cannot be detected and without the form no PDF can be generated.");
  16:     }
  17:     throw new ApplicationException("Required XML processing instruction (mso-infoPathSolution) not found. Without this, the location of the form cannot be detected and without the form no PDF can be generated.");
  18: }

The most annoying part is, that the value of a processing instruction can be anything. In this case, it appears to be a list of attributes like “normal” XML, but since this is not guaranteed, there is no language support for getting the value of the href “attribute”. So I chose to use string manipulation to get the value.

After getting the href, I need to get the XSN file from SharePoint Server, where the form is published. This turned out to be a challenge also.

My first approach was quite simple:

   1: private static byte[] GetFormByUrl(string href)
   2: {
   3:     var wc = new WebClient
   4:     {
   5:         Credentials = CredentialCache.DefaultCredentials
   6:     };
   7:     return wc.DownloadData(href);
   8: }

This turned out to be something silly, though. What happens when SharePoint and Forms Server get a request for the XSN file, it assumes some one is trying to fill out the form. So what I got back was the HTML that the Forms Server was sending a user that wanted to fill out the form. Then I thought I’d try to do this:

   1: private static byte[] GetFormByUrl(string href)
   2: {
   3:     HttpWebRequest wr = (HttpWebRequest)HttpWebRequest.Create(href);
   4:     wr.AllowAutoRedirect = false;
   5:     WebResponse resp = wr.GetResponse();
   6:     Stream stream = resp.GetResponseStream();
   7:     using (MemoryStream ms = new MemoryStream())
   8:     {
   9:         byte[] buffer = new byte[1024];
  10:         int bytes = 0;
  11:         while ((bytes = stream.Read(buffer,0, buffer.Length)) != -1)
  12:             ms.Write(buffer,0,bytes);
  13:         return ms.ToArray();
  14:     }
  15: }

Basically, using an HttpWebRequest I could ask it to not redirect. This didn’t work either, since what I then got back was some HTML that basically just said that the page has moved. Bummer.

But then another colleague who apparently is better at searching than I am found out that I can add a noredirect parameter to my request that will instruct SharePoint to not redirect. This is different from my current approach because my current approach instructs .NET to not follow redirects, whereas this new approach instructs SharePoint to not ask me to redirect.

So I ended up with something as simple as this:

   1: private static byte[] GetFormByUrl(string href)
   2: {
   3:     string url = href + "?noredirect=true";
   4:     var wc = new WebClient
   5:     {
   6:         Credentials = CredentialCache.DefaultCredentials
   7:     };
   8:     return wc.DownloadData(url);
   9: }

Simple and beautiful :-)

Now I have the XSN file and the next issue pops up, naturally; How do I get the XSL extracted from the XSN file. The XSN file is just a cabinet file with another extension, so I thought this must be easy. I found out it is not. I searched and searched and ended up finding all sorts of weird stuff where people used p/invoke to do stuff and what not. I am confused that Microsoft have not added at least extraction functionality to the .NET framework, but they haven’t.

I ended up doing this:

   1: private static string ExtractCabFile(string cabFile)
   2: {
   3:     string destDir = CreateTmp(true, "");
   4:  
   5:     var sh = new Shell();
   6:     Folder fldr = sh.NameSpace(destDir);
   7:     foreach (FolderItem f in sh.NameSpace(cabFile).Items())
   8:         fldr.CopyHere(f, 0);
   9:     return destDir;
  10: }

This code assumes that the XSN file has been written to a temporary file with the extension .CAB – this is very important, since the shell command will open up the .CAB file with the default program, which is then the explorer. After that, all files in the cabinet file is copied to “destDir” which is just a directory created in the users Temp directory.

I am quite annoyed to have to go through all this, but that’s how things go sometimes.

So now I have found the href of the form, downloaded the form and extracted its files. Time for the transformation:

   1: private static MemoryStream PerformTransformation(XmlDocument xmldoc, string destDir, string view)
   2: {
   3:     var transform = new XslCompiledTransform();
   4:     var stream = new StreamReader(destDir + @"\" + view + ".xsl");
   5:     XmlReader xmlReader = XmlReader.Create(stream);
   6:     transform.Load(xmlReader);
   7:  
   8:     var outputMemStream = new MemoryStream();
   9:     transform.Transform(xmldoc, null, outputMemStream);
  10:     stream.Close();
  11:     xmlReader.Close();
  12:     outputMemStream.Seek(0, SeekOrigin.Begin);
  13:     return outputMemStream;
  14: }

So just a normal XSLT transformation, resulting in some HTML that is returned in a stream.

After this, I need to convert it into PDF, which is really simple using a tool we bought for this:

   1: private static byte[] GetPdfFromHtml(Parameters param)
   2: {
   3:     var pdfConverter = new PdfConverter
   4:     {
   5:         LicenseKey = "SomethingElse - You are not getting the correct License Key :-)"
   6:     };
   7:  
   8:     byte[] pdfBytes = pdfConverter.GetPdfBytesFromHtmlStream(param.HtmlStream, Encoding.UTF8, param.DestDir.EndsWith(@"\") ? param.DestDir : param.DestDir + @"\");
   9:     return pdfBytes;
  10: }

We are using the ExpertPDF library for this. The third parameter for the GetPdfBytesFromHtmlStream method call is the directory where the cabinet file was extracted to, since this is where all images used in the form are also kept and they are needed for the PDF to include them.

All in all; the component now works, but it turned out to be a lot more difficult than I had hoped.

As a last detail, I added a property to my pipeline component that the developer can use to decide which view to use for the transformation form XML to HTML.

The complete code for the pipeline component will not be available for download, since this was done for a customer, but I might do something a bit smaller and simpler and add it to my pipeline component collection later on.

--

eliasen

Saturday, 10 October 2009 16:32:03 (Romance Daylight Time, UTC+02:00)  #    Comments [1]  | 
Friday, 09 October 2009

Hi all

It has arrived – the first service pack for BizTalk 2006 R2.

Get it here: https://connect.microsoft.com/site/sitehome.aspx?SiteID=65

For a list of fixes, see here: http://support.microsoft.com/kb/974563

New features, see here: http://msdn.microsoft.com/en-us/library/ee532481(BTS.20).aspx

Good luck with it.

--
eliasen

Friday, 09 October 2009 20:22:28 (Romance Daylight Time, UTC+02:00)  #    Comments [0]  | 
Saturday, 03 October 2009

Hi all

Jon Skeet, who has been a C# MVP since 2003, as I understand it was to have his MVP renewed on October 1’st 2009, but his employer Google advised him not to be renewed, so he had to tell Microsoft to not consider him for renewal. You can see Jons blog entry here.

To be honest, I think it is very petty way Google is thinking.

So to Jon: Great work, keep it up, and hopefully either Google will come around or you will find a better job and get your MVP status back.

--
eliasen

Saturday, 03 October 2009 14:40:03 (Romance Daylight Time, UTC+02:00)  #    Comments [0]  | 

Hi all

Now, I am aware that people following my blog are probably very likely to follow Richard Seroters blog as well, but just in case you haven’t noticed, Richard tortured me with his 4 questions this time :-)

You can find his questions and my answers here.

Now, being interviewed by Richard and thus having a link to my blog appear on his blog entry is bound to generate some traffic to my blog from people who either didn’t know me or didn’t visit my blog… So naturally, the first about 12 hour after Richard posted his interview, my blog was down because the webhosting company that hosts eliasen.dk had changed something… or something else, who knows? :-)

As you can see, though, my blog is again up and running and will hopefully stay this way! :-)

--
eliasen

Saturday, 03 October 2009 14:22:35 (Romance Daylight Time, UTC+02:00)  #    Comments [0]  | 
Friday, 18 September 2009

Hi all

I am REALLY excited to announce, that I will be co-authoring a book on BizTalk. I will be a part of a terrific team consisting of

  • Anush Kumar
  • Brian Loesgen
  • Charles Young
  • Jon Flanders
  • Scot Colestock
  • Tom Canter
  • Me :-)

Together we will be writing “BizTalk Server 2009 Unleashed”, which is so new, that you cannot find it on the web page of the publisher or any other sites. It is so new, that we haven’t even signed our contracts with the publisher yet, which may cause someone to quit the project if they are not happy about the contract… so nothing promised yet.

But, needless to say, I am really excited, and giving the team, also feeling quite humble :-).

This will be my first book.

Wohooo

--
eliasen

Friday, 18 September 2009 22:06:38 (Romance Daylight Time, UTC+02:00)  #    Comments [11]  | 

Hi all

So, sorry to see it, but it appears it has been almost two months since my last blog post – hope to get back on track soon.

Since the last time, I have installed Windows 7 RTM on my laptop, and one of the reasons I haven’t blogged is, that I couldn’t get Windows Live Writer Backup to restore my backup of Windows Live Writer from my old Windows XP installation. That turned out to be a silly thing… For others; You cannot restore using WLW Backup without having run Windows Live Writer first. It isn’t enough to install it, it must have been run also. Oh well.

 

I hope to entertain you all some more from now on…

 

--
eliasen

Friday, 18 September 2009 21:55:49 (Romance Daylight Time, UTC+02:00)  #    Comments [0]  | 

Hi all

Some weeks ago, I had a customer that had an Order XML and needed to fetch all the names of the ordered items based on the item number that was in the XML.

He contacted me because the solution he had thought of didn’t work. What he did was that he mapped the Order XML to a SQL Adapter schema that called an SP in SQL Server to get the item name based on the item number. The issue he ran into was, that the SP got called multiple times – once for each item, and the SQL adapter didn’t seem to batch all the results into one result for his orchestration.

So, given this simplified Order:

<ns0:Order xmlns:ns0="http://MultipleCallsToSP.Order">
  <Header>
    <CustomerName>CustomerName_0</CustomerName>
    <OrderNumber>OrderNumber_0</OrderNumber>
  </Header>
  <OrderLines>
    <OrderLine>
      <ItemNumber>21</ItemNumber>
      <Quantity>42</Quantity>
    </OrderLine>
    <OrderLine>
      <ItemNumber>42</ItemNumber>
      <Quantity>21</Quantity>
    </OrderLine>
  </OrderLines>
</ns0:Order>

He mapped it to this XML:

<ns0:GetItemNameRequest xmlns:ns0="http://eliasen.dk">
  <ns0:GetItemName ItemID="21" />
  <ns0:GetItemName ItemID="42" /> 
</ns0:GetItemNameRequest>

This was then sent to SQL Server using the SQL Adapter to call a SP named “GetItemName” which just takes an ItemID (int) as parameter and returns the ItemName hat matches the ItemID.

Now, the schema that is generated for the SQL Server request actually doesn’t allow for multiple GetItemName elements to be created, but that is changeable :-) If you set it to have maxOcurs = unbounded, then it can occur multiple times, and what happens is that the SP is called multiple times. Unfortunately, only one of the ItemNames is returned – the rest is ignored.

So the customer came to me because naturally, he needed all the ItemNames and not just one of them. I have suggested 5 possible solutions, which I will describe here.

First option
Use the pattern described at http://blog.eliasen.dk/2006/11/05/LoopingAroundElementsOfAMessage.aspx to loop around the order lines and build the resulting XML one order line at a time.

Second option
Use enveloping in the receive location in order to get one orchestration started for each order line.

Third option
Use the Database Lookup functoid to retrieve the ItemName based on the ItemID

Fourth option
Generate a comma separated list of ItemID’s in the map, and let the stored procedure use that list to return the relevant ItemNames. This has some consequences for the stored procedure. Before it looked like this:

SELECT ItemNumber, ItemName
FROM Items
WHERE Items.ItemNumber = @ItemID
FOR XML AUTO, ELEMENTS

Now, it looks like this:

SELECT ItemNumber, ItemName
FROM Items
WHERE EXISTS (select * from dbo.Split(‘,’,@items) where [Items].ItemNumber = ID)
FOR XML AUTO, ELEMENTS

@items is the parameter for the SP, which is just an nvarchar that is to contain the comma separated list.

For this to work you need the Split function, which looks like this:

CREATE FUNCTION [dbo].[Split] (@sep char(1), @s varchar(512))
RETURNS table
AS
RETURN
(    
                WITH Pieces(pn, start, stop) AS (
                      SELECT 1, 1, CHARINDEX(@sep, @s)
                      UNION ALL      
                      SELECT pn + 1, stop + 1, CHARINDEX(@sep, @s, stop + 1)      
                      FROM Pieces      
                      WHERE stop > 0    
                 )
                 SELECT pn, CONVERT(int, SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END)) AS ID
FROM Pieces
)

In order to generate the comma separated list in your map, I have written two blog posts about this issue, which you can find at http://blog.eliasen.dk/2009/06/22/HandlingCommaSeparatedValuesInsideAMapPartI.aspx and http://blog.eliasen.dk/2009/06/27/HandlingCommaSeparatedValuesInsideAMapPartII.aspx

Fifth option
The fifth and last option i want to mention is, that with the new SQL Server LOB adapter from Adapter Pack 2.0, it appears that you can do it like the customer wanted to do it in the first place with sending one XML to SQL Server and getting an accumulated response back from SQL Server based on several calls to a stored procedure. I haven’t had time to test this, but look out for another blog post about this :-)

 

Hope this helps someone.

 

--
eliasen

Friday, 18 September 2009 21:47:51 (Romance Daylight Time, UTC+02:00)  #    Comments [0]  | 
Tuesday, 28 July 2009

Hi all

Just to let you know, I am now listed on http://www.biztalkblogs.com along with lots of other cool BizTalk bloggers :-)

--
eliasen

Tuesday, 28 July 2009 17:40:29 (Romance Daylight Time, UTC+02:00)  #    Comments [0]  | 
Thursday, 23 July 2009

Hi all

I have just released version 5 of my pipeline components library.

It has the following additions:

  • SuspendAfterMap. In BizTalk 2009 (which is the only supported BizTalk version for this pipeline component) there has been added support for recoverable interchanges for errors occurring during mapping on receive ports after disassembling. This is achieved by setting a specific promoted property to “true”. I have created a pipeline component that will do this for you.
  • WriteProperties. This pipeline component serves almost NO purpose at all, except I used it for debugging to see what properties existed on a message going through BizTalk. It will write out all context of a message to the eventlog, one event at a time.

It has the following new features:

  • Promote. The pipeline component used for promoting a value based on an XPath expression and thereby enabling you to promote a specific instance of a reoccurring element has been enhanced by a “constant”. So instead of having to set the value of some property to the result of an XPath expression you can just enter a constant instead. If you enter both a constant and an XPath expression the constant wins. This new feature is quite handy if you need to set the value of for instance MIME.FileName to a specific value or any other property for that matter.

You can find the newest versions at http://eebiztalkpipelinecom.codeplex.com/

--
eliasen

Thursday, 23 July 2009 00:26:18 (Romance Daylight Time, UTC+02:00)  #    Comments [2]  | 
Wednesday, 22 July 2009

Hi all

I have just released version 9.1 of my functoid library. Actually, nothing much new has appeared. I had accidentally left out the functoid I described at http://blog.eliasen.dk/2009/03/08/SolvingTheIfThenElseProblemInAMapPartIII.aspx from version 9, so now it is reintroduced – and it has also been added to the documentation.

Find the newest version of the functoids library at http://eebiztalkfunctoids.codeplex.com – thanks.

--
eliasen

Wednesday, 22 July 2009 23:59:05 (Romance Daylight Time, UTC+02:00)  #    Comments [0]  | 

Theme design by Jelle Druyts