Printing the Display View of an InfoPath List Item Form

Applies To: SharePoint 2010

I’ve written previously about a cool feature in SharePoint 2010 Server Enterprise that allows you to customize list item forms using InfoPath. It’s really simple to do and you can get some pretty cool results in just a couple of minutes. For instance, I posted a while back about how to use SharePoint column validation to validate email addresses and phone numbers. Those are still good techniques, but by using an InfoPath list item form it’s just a validation drop down (you can even do regular expressions) and you’re done!

So the ease of validation, conditional hiding of fields, etc. are all pretty useful. However, the thing I like it most for is the ability to use different InfoPath views to match the list item views. So you can have different columns available when you’re editing than when you’re adding a new item, for instance. I especially like to spruce up the Display form.

(For some quick tips on how to get the different views working check out the top of my old post)

So, let’s say you’ve got a nice looking display form. Users open that thing up and decide to print. There’s no button, so they use the print button in the browser. Generally they’ll end up with some mess that prints all of your branding, usually some of the list behind the modal dialog, and if you’re lucky mixed in there somewhere will be your display form. Obviously, that’s not going to cut it.

So I did some digging and found some examples of people using javascript to print the form and their solutions were pretty intriguing. But I didn’t particularly want to have to apply some javascript to every form or to have to add a content editor to the pages, etc. I wanted something that just worked on existing forms and new ones too. So I did a little research into Ribbon customization and came across this great series by Chris O’Brien.

I put it all together in a solution and put it over on CodePlex as WireBear InfoPath Printer. There’s some stuff about it’s license over there (Free for personal and commercial use, etc.) and the basic installation instructions. It’s super easy to setup since it’s just a standard SharePoint Solution that you globally deploy.

You can find the full source code over on CodePlex. It’s not too complex and I’ll probably explain most of it in the next couple of posts. Bottom line is that it adds a Print button to the Ribbon when viewing list items that use an InfoPath Form:

The final printout only shows the form (No Ribbon, No Header, No Footer, No QuickLaunch, etc.).

The button is added using Custom Action XML that is deployed as a feature in the solution. The XML is targeted to allow the button to only be present when Viewing a List Item using an InfoPath Browser based List Form.

When you click the button, standard JavaScript is executed to find the InfoPath div element on the page and to copy the form’s HTML into a new window (along with all standard CSS/script references already present) and uses the browser’s page printing. Once the print dialog closes, so does the window.

We’ve been using it around here for a while and almost no one even knows it’s a custom solution. It looks like part of the UI and it’s use is immediately understood. So, go get it (It’s free!) and let me know what you think.

Updating an XML File in the 14 Hive Using a Custom Timer Job

Applies To: SharePoint 2010, .NET Framework (C#, VB.NET)

As mentioned in a previous post, I’ve recently put together a solution for automatically configuring your SharePoint servers to use the Adobe PDF icon for PDF files. You can download the solution as well as the source for free from CodePlex here: WireBear PDFdocIcon. I’m going to show some of the code as it currently exists below, but be sure to check out the CodePlex site to ensure you have the latest version.

I’ve also provided the bulk of the code and some explanation for installing/uninstalling a custom job from a SharePoint solution in my last post: Implementing a Custom SharePoint Timer Job. In this post we’ll explore what’s actually happening in the execution of the timer job.

The goal is to update the DOCICON.xml file in the 14\TEMPLATE\XML folder within the SharePoint 2010 Hive to include or remove a mapping entry for a specific file extension. Here is the entire DocIconJob class:

The Code:

Imports Microsoft.SharePoint.Administration
Imports System.IO
Imports Microsoft.SharePoint.Utilities
Imports System.Xml

Public Class DocIconJob
    Inherits SPServiceJobDefinition

#Region "Properties"

    Private _dociconPath As String
    Public ReadOnly Property DocIconPath() As String
        Get
            If String.IsNullOrEmpty(_dociconPath) Then _dociconPath = SPUtility.GetGenericSetupPath("TEMPLATE\XML\DOCICON.XML")
            Return _dociconPath
        End Get
    End Property

    Private Const InstallingKey As String = "DocIconJob_InstallingKey"
    Private Property _installing() As Boolean
        Get
            If Properties.ContainsKey(InstallingKey) Then
                Return Convert.ToBoolean(Properties(InstallingKey))
            Else
                Return True
            End If
        End Get
        Set(ByVal value As Boolean)
            If Properties.ContainsKey(InstallingKey) Then
                Properties(InstallingKey) = value.ToString
            Else
                Properties.Add(InstallingKey, value.ToString)
            End If
        End Set
    End Property

    Private Const FileExtensionKey As String = "DocIconJob_FileExtensionKey"
    Private Property _fileExtension() As String
        Get
            If Properties.ContainsKey(FileExtensionKey) Then
                Return Convert.ToString(Properties(FileExtensionKey))
            Else
                Return String.Empty
            End If
        End Get
        Set(ByVal value As String)
            If Properties.ContainsKey(FileExtensionKey) Then
                Properties(FileExtensionKey) = value
            Else
                Properties.Add(FileExtensionKey, value)
            End If
        End Set
    End Property

    Private Const ImageFilenameKey As String = "DocIconJob_ImageFilenameKey"
    Private Property _imageFilename() As String
        Get
            If Properties.ContainsKey(ImageFilenameKey) Then
                Return Convert.ToString(Properties(ImageFilenameKey))
            Else
                Return String.Empty
            End If
        End Get
        Set(ByVal value As String)
            If Properties.ContainsKey(ImageFilenameKey) Then
                Properties(ImageFilenameKey) = value
            Else
                Properties.Add(ImageFilenameKey, value)
            End If
        End Set
    End Property

#End Region

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(JobName As String, service As SPService, Installing As Boolean, FileExtension As String, ImageFilename As String)
        MyBase.New(JobName, service)
        _installing = Installing
        _fileExtension = FileExtension
        _imageFilename = ImageFilename
    End Sub

    Public Overrides Sub Execute(jobState As Microsoft.SharePoint.Administration.SPJobState)
        UpdateDocIcon()
    End Sub

    Private Sub UpdateDocIcon()
        Dim x As New XmlDocument
        x.Load(DocIconPath)

        Dim mapNode As XmlNode = x.SelectSingleNode(String.Format("DocIcons/ByExtension/Mapping[@Key='{0}']", _fileExtension))

        If _installing Then
            'Create DocIcon entry
            If mapNode Is Nothing Then
                'Create Attributes
                Dim keyAttribute As XmlAttribute = x.CreateAttribute("Key")
                keyAttribute.Value = _fileExtension
                Dim valueAttribute As XmlAttribute = x.CreateAttribute("Value")
                valueAttribute.Value = _imageFilename

                'Create Node
                mapNode = x.CreateElement("Mapping")
                mapNode.Attributes.Append(keyAttribute)
                mapNode.Attributes.Append(valueAttribute)

                Dim byExtensionNode = x.SelectSingleNode("DocIcons/ByExtension")
                Dim NodeAdded As Boolean = False
                If byExtensionNode IsNot Nothing Then
                    'Add in alphabetic order
                    For Each mapping As XmlNode In byExtensionNode.ChildNodes
                        If mapping.Attributes("Key").Value.CompareTo(_fileExtension) > 0 Then
                            byExtensionNode.InsertBefore(mapNode, mapping)
                            NodeAdded = True
                            Exit For
                        End If
                    Next

                    If Not NodeAdded Then byExtensionNode.AppendChild(mapNode)
                    x.Save(DocIconPath)
                End If
            End If
        Else
            'Remove DocIcon entry
            If mapNode IsNot Nothing Then
                Dim byExtensionNode = x.SelectSingleNode("DocIcons/ByExtension")
                If byExtensionNode IsNot Nothing Then
                    byExtensionNode.RemoveChild(mapNode)
                    x.Save(DocIconPath)
                End If
            End If
        End If
    End Sub

End Class

What’s Going On:

Lines 9-73 are just the declaration of and logic needed to persist some properties. Again more information can be found in my last post, but basically I am using the SPJobDefinition’s Properties HashTable to store my own properties as specified in the constructor. Except for in the case of the DocIconPath property which is really just wrapping up some logic to get a reference to the 14 Hive’s TEMPLATE\XML directory using the SPUtility class.

The Execute method beginning in line 86 is what is called when the Timer Job actually runs. I override this method to ensure my custom code gets called instead. My custom code really begins in the UpdateDocIcon method starting at line 90.

In lines 91-94, I load the DOCICON.xml file into and XmlDocument object and attempt to find the mapping node that applies to the appropriate file extension (In this case it’s going to be pdf).

If this job is installing (Running on Solution Activation), then I just check to see if the node was found. If so, all done! If not, then it’s time to add it. I create the node and setup it’s attributes in lines 100-108 using standard objects from the System.Xml namespace.

In order to work, the mapping node needs to be added as a child of the ByExtension element, so we find that in line 110. By default the mapping nodes are listed in alphabetical order by their extension. Since I’m anal, I use a method in lines 114-120 presented by Steve Goodyear to ensure I insert the mapping node in it’s proper position. Failing that, I add it to the end in Line 122 and save the file in line 123.

If this job is uninstalling (Running on Solution Deactivation) and the mapping exists, we delete it and save the file in lines 128-134.

Isn’t that Super Exciting?!?! Hopefully this example will help make the concepts I was talking about in my previous post make some sense. If not, then sadness will fill my soul and flowers will no longer bloom or something.

Implementing a Custom SharePoint Timer Job

Applies To: SharePoint 2010

As mentioned in my previous post, I’ve recently put together a solution for automatically configuring your SharePoint servers to use the Adobe PDF icon for PDF files. You can download the solution as well as the source for free from CodePlex here: WireBear PDFdocIcon. I’m going to show some of the code as it currently exists below, but be sure to check out the CodePlex site to ensure you have the latest version.

In order to perform the necessary work on each server in the farm, the PDFdocIcon solution uses a custom Timer Job. This post will focus on the plumbing necessary to setup your own custom timer job that runs on every server in the farm. The actual code to change the DOCICON.XML file will be saved for later.

Choosing Your Job Definition Type

To make your own Timer Job you’ll want to subclass an exisiting Job Definition object and override the Execute method. There are several to choose from, here’s a helpful table:

Job Definitions you can inherit from in the Microsoft.SharePoint.Administration namespace:
SPAdministrationServiceJobDefinition Invokes the SharePoint Administration Service
SPAllSitesJobDefinition Iterates through all sites in a Web Application
SPContentDatabaseJobDefinition Executed per Web Application and each Content Database is processed by individual jobs (Pausable)
SPFirstAvailableServiceJobDefinition Timer Job that runs on the first available server where the specified service exists (Pausable)
SPJobDefinition Base Class for Timer Jobs (Generally, this is the one to use)
SPPausableJobDefinition Timer Job that can be paused
SPServerJobDefinition Executed on a specific server (Pausable)
SPServiceJobDefinition Runs on every server in the farm where the service exists (Pausable) – This is the one I chose
SPWorkItemJobDefinition  Works with the Timer Job to process work items (Pausable)

For simple jobs the SPJobDefinition is the most flexible and is what you’ll generally want to use. For the PDFdocIcon solution, I needed the Timer Job to execute on every server in the farm. So I used the SPServiceJobDefinition and specified the Timer Service.

Storing Persistent Properties

You may not need properties, but if you’re doing anything even mildly complex you probably will. There are a couple of different alternatives here, but basically your properties need to serialize down to strings. You can look up a few examples of custom properties objects that do this, or you can just use my method of storing your properties in the JobDefinition’s Properties object (HashTable).

Here’s how I store the Boolean property _installing:

    Private Const InstallingKey As String = "DocIconJob_InstallingKey"
    Private Property _installing() As Boolean
        Get
            If Properties.ContainsKey(InstallingKey) Then
                Return Convert.ToBoolean(Properties(InstallingKey))
            Else
                Return True
            End If
        End Get
        Set(ByVal value As Boolean)
            If Properties.ContainsKey(InstallingKey) Then
                Properties(InstallingKey) = value.ToString
            Else
                Properties.Add(InstallingKey, value.ToString)
            End If
        End Set
    End Property

Basically, you have a String key for each property that you use to store/retrieve the value from the Properties HashTable. By wrapping those calls in a property you can treat it like a standard variable in the rest of your code and forget all about the specialized storage/retrieval required.

Constructors

You are required to have an empty (parameterless) constructor for serialization, so make sure you’ve got that:

    Public Sub New()
        MyBase.New()
    End Sub

But you will probably need to implement at least a matching constructor with some custom properties. In my Timer Job, I wanted to pass three properties (which I then store using the method above), so I use this:

    Public Sub New(JobName As String, service As SPService, Installing As Boolean, FileExtension As String, ImageFilename As String)
        MyBase.New(JobName, service)
        _installing = Installing
        _fileExtension = FileExtension
        _imageFilename = ImageFilename
    End Sub

Execution

Depending on the base Job Definition class you chose, the Execute method may have a slightly different signature, but either way this is the method to override to provide your own custom logic. In an SPServiceJobDefinition subclass the signature looks like this:

    Public Overrides Sub Execute(jobState As Microsoft.SharePoint.Administration.SPJobState)
        'Custom code here!!
    End Sub

Installing Your Job with a Solution

Using Visual Studio you can create a new Empty SharePoint Project and add your Timer Job class to it. To deploy it you’ll need to add a Feature (Right-click on Features and choose Add Feature). To install your job, you’ll need to add an Event Receiver (Right-click on your new Feature and choose Add Event Receiver).

Uncomment the FeatureActivated and FeatureDeactivating methods. Create a new method (Mine is named RunDocIconJob) with a Boolean and SPFeatureReceiverProperties parameters. This will be the method where we install or uninstall your custom job. In your FeatureActivated and FeatureDeactivating methods call this new method accordingly:

    Public Overrides Sub FeatureActivated(properties As Microsoft.SharePoint.SPFeatureReceiverProperties)
        RunDocIconJob(True, properties)
    End Sub

    Public Overrides Sub FeatureDeActivating(properties As Microsoft.SharePoint.SPFeatureReceiverProperties)
        RunDocIconJob(False, properties)
    End Sub

Then your Job method will look something like this:

    Private _fileExtension As String = "pdf"
    Private _iconFileName As String = "ICPDF.png"

    Public Sub RunDocIconJob(Installing As Boolean, properties As SPFeatureReceiverProperties)
        Dim JobName As String = String.Format("DocIconJob_{0}", _fileExtension)

        'Ensure job doesn't already exist (delete if it does)
        Dim query = From job As SPJobDefinition In properties.Definition.Farm.TimerService.JobDefinitions Where job.Name.Equals(JobName) Select job
        Dim myJobDefinition As SPJobDefinition = query.FirstOrDefault()
        If myJobDefinition IsNot Nothing Then myJobDefinition.Delete()

        Dim myJob As New DocIconJob(JobName, SPFarm.Local.TimerService, Installing, _fileExtension, _iconFileName)

        'Get that job going!
        myJob.Title = String.Format("{0} icon mapping for {1}", IIf(Installing, "Adding", "Removing"), _fileExtension)
        myJob.Update()
        myJob.RunNow()
    End Sub

This is the method I use for my SPServiceJobDefinition. I am not doing any kind of scheduling since this job just runs once on initial deployment and once when being removed. However, you may want to adjust your method to include a schedule (Just set the myJob.Schedule parameter before the Update() call).

Lines 5-10 are finding any existing job definitions that share the same name and deleting them since creating jobs with duplicate names will cause an error. The Title doesn’t have to be unique, but the name does.

Line 12 actually creates the job with my default parameters and then line 15 sets a Title. This is where you would introduce a schedule if you wanted the job to run more than once, but if not just call Update() to save your job. I want my job to run immediately, so in line 17 I call the RunNow() method to do exactly that.

That’s it! You now have a shell for setting up and installing a custom job – specifically one that runs on every server in the farm. My next post will cover what I’m actually doing in the Execution to ensure the DOCICON.xml file is updated appropriately.

Quick Note about testing: In many cases you will need to either restart the Timer Job Service on each server or change your Assembly Version number to get the timer job to pick up any code changes. This doesn’t always happen, but it happens enough to be annoying.

Automatically Setting Up PDF Icon Mapping in SharePoint 2010

Applies To: SharePoint 2010

Nearly everyone who has ever used SharePoint has had to setup the PDF icon mapping so that PDF documents will have the familiar Adobe logo rather than the blank, unknown icon SharePoint uses by default. This is relatively simple and there are guides to do doing this all over the internet. (Microsoft’s can be found here).

Here is a very brief summay of the steps that must be performed manually on every server:

  1. Copy the PDF icon picture from Adobe and put it in your 14 Hive (TEMPLATE\IMAGES)
  2. Edit the DOCICON.xml file in your 14 Hive (TEMPLATE\XML) to add a Mapping element for pdf documents pointing to your new icon
  3. Reset IIS

These aren’t super complicated steps but there are some pretty big problems (or at least irritations) with using this approach:

  • Manual changes can often be error-prone, especially for those not familiar with XML
  • The change must be performed on every server
  • The change must be performed whenever a new server is added to the farm
  • The change will have to be redone in the event of disaster recovery

So, like many before me, I thought, surely this can all be automated! So I looked and I found some solutions for SharePoint 2007 and several solutions that only worked for Standalone Servers or for only one server in the farm. These were of help, but still no good for my needs. So, I wrote my own.

You can find it over on CodePlex as WireBear PDFdocIcon. There’s some stuff about it’s license over there (Free for personal and commercial use, etc.) and the basic installation instructions. It’s super easy to setup since it’s just a standard SharePoint Solution that you globally deploy.

The full source code is available on CodePlex, but I’ll be going in depth about how it works over the next few posts. But to summarize, here’s what happens:

  • The Adobe PDF icon file is copied to the 14\TEMPLATE\IMAGES folder using standard resource deployment
  • On Activation and Deactivation a one time Service Timer Job is run.
  • On Activation, the Timer job searches for a mapping for PDF documents within the 14\TEMPLATE\XML\DOCICON.xml file. If not found, it adds one (in alphabetic order) and points it to the icon file
  • An IIS Reset is performed to get the changes activated
  • When Deactivating, the Timer job removes the mapping for PDF documents
So why use this thing?
  • The changes will be reapplied in the event of Disaster Recovery
  • The changes will be applied to new servers as they are added to your farm
  • You don’t have to personally edit the 14 Hive on every server in your farm
  • It makes a special place in your heart of hearts that keeps the beast at bay

In making this, I came across several blog entries that were especially helpful, here are most of these (Thanks!):

I’ve found this to be a helpful approach and I hope you do too.

Batch Updates with SharePoint Web Services

Applies To: SharePoint

One of the biggest irritations to me coming from a SQL background is trying to do SQL like things in SharePoint 2007. One of those irritations is performing a simple update through the web services.  In SQL, I could write something like:

UPDATE ListTable
    SET FieldName = FieldValue
    WHERE QueryFieldName = QueryFieldValue

In the generic T-SQL above, any row where the QueryFieldName column had a value equal to the QueryFieldValue would have it’s FieldName Column set to FieldValue. Pretty straightforward for anyone with a database background.

With the SharePoint Web Services, however, there isn’t a simple UPDATE command like this. Instead of the one simple command above, we have to make 2 calls to the Web Service and provide some complicated XML parameters. This can be a big hassle the first time you do this, so to hopefully save you some headaches, I’ll provide most of the code required below.

I won’t be showing you how to setup whatever project you are using or how to connect to the web service. The code is in C# and I will assume your service reference (Lists) is called myService.  So let’s get started!

Helper Functions:

I will be using a few helper functions to make generating the XML parameters easier, I’ll go ahead and list these here:

private void AddAttribute(XmlDocument x, ref XmlNode node,
          string attributename, string attributevalue,
          string AttributeNameSpace = "")
{
    XmlNode attnode =
              x.CreateNode(XmlNodeType.Attribute, attributename,
              AttributeNameSpace);
    attnode.Value = attributevalue;
    node.Attributes.Append(attnode);
}

private XmlNode CreateUpdateNode(XmlDocument x, int ID, string RowID)
{
    XmlNode node = x.CreateNode(XmlNodeType.Element, "Method", null);
    AddAttribute(x, node, "ID", ID);
    AddAttribute(x, node, "Cmd", "Update");
    node.AppendChild(CreateFieldNode(x, "ID", RowID));
    return node;
}

private XmlNode CreateFieldNode(XmlDocument x, string Name, string Value)
{
    XmlNode node = x.CreateNode(XmlNodeType.Element, "Field", null);
    AddAttribute(x, node, "Name", Name);
    node.InnerText = Value;
    return node;
}

Retrieve the Items to be Updated:

This is equivalent to the WHERE clause of the example SQL query. You will need to use the GetListItems method of the Lists service.

This method uses the List GUID to reference the target list, I’ll leave it to you to retrieve it and will assume it in the variable ListGUID. You will also need to replace the QueryFieldName, QueryFieldType, and QueryFieldValue variables with appropriate values.

XmlDocument doc = new XmlDocument();
XmlNode queryNode = doc.CreateNode(XmlNodeType.Element, "Query", null);
queryNode.InnerXml =
          string.Format("<Where><Eq><FieldRef Name='{0}' />
                         <Value Type='{1}'>{2}</Value></Eq></Where>",
          QueryFieldName, QueryFieldType, QueryFieldValue);

XmlNode viewNode = doc.CreateNode(XmlNodeType.Element, "ViewFields", null);
viewNode.InnerXml = "<FieldRef Name='ID'/>";

XmlNode resultsNode = myService.GetListItems(ListGUID, null, queryNode,
          viewNode, null,
          doc.CreateNode(XmlNodeType.Element, "QueryOptions", null), null);

 Parse the IDs from the Results:

XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("z", "#RowsetSchema");
XmlNodeList rowNodes = resultsNode.SelectNodes(".//z:row", nsmgr);
List<int> IDs = new List<int>();
foreach (XmlNode rowNode in rowNodes) {
    XmlAttribute result = rowNode.Attributes("ows_ID");
    IDs.Add(int.Parse(result.Value));
}

Perform the Update:

This is equivalent to the SET portion of the example SQL query. You will need to use the UpdateListItems method of the Lists service.

Again, this method uses the List GUID to reference the target list, I’ll leave it to you to retrieve it and will assume it in the variable ListGUID. You will also need to replace the FieldName and FieldValue variables with appropriate values.

<pre>XmlDocument doc2 = new XmlDocument();
XmlNode node = doc2.CreateNode(XmlNodeType.Element, "Batch", null);
AddAttribute(doc2, node, "OnError", "Continue");

int itemCount = 1;
foreach (int i in IDs) {
    XmlNode updateNode = CreateUpdateNode(doc2, itemCount, i);
    updateNode.AppendChild(CreateFieldNode(doc2, FieldName, FieldValue));
    //Append More children here to update multiple fields
    node.AppendChild(updateNode);
    itemCount += 1;
}

myService.UpdateListItems(ListGUID, node);

That’s it. To review, we created a CAML query and retrieved the IDs of the matching rows using the GetListItems method. We parsed the XML result and put all of those IDs in a List<int>. We then added each row ID to a batch update XML parameter which we used in the UpdateListItems method.

As you can see, this is way more complicated and not nearly as obvious as a SQL UPDATE command, but you can take the above code and create a pretty generic wrapper to make your web service updates nearly as easy. Let me know how it works out for you or if you have any questions!

Originally Published on WireBear.com/blog on February 11, 2011