Whew, that was a long title – But I think you’ll find this very useful. Some notes on deploying a custom workflow action that you can use inside of SharePoint designer, which will Zip attachments to list items and deposit the compressed file in a document library – and it is all deployed in a sandboxed solution, so you can use it on hosted SharePoint Foundation instances. In this case, I even generate PDF files with the data from the list items, and include them in the compressed file. Great for archival or offsite storage.
First of all, this MSDN article about general workflow actions and sandboxed solutions will help you immensely with the basics:
Next you’ll want to grab a copy of:
- SharpZipLib (an open source compression library)
- SharpPDF (if you want to generate PDF’s too)
You’ll want to get the source to each, as we’ll upgrade them to .net 4.0, and build them as signed assemblies.
Make yourself a SharePoint solution in Visual Studio per the MSDN article above, and add two (or one if you are leaving out the PDF bits) existing projects using the source of the libraries (SharpZip, SharpPDF).
- You’ll need to let the upgrade wizard upgrade them to .net 4.
- Check the project settings on them and make sure they are targeting the .net 4 framework (AnyCPU)
- For SharpPDF you’ll need to open the AssemblyInfo.cs file and add a line to the bottom
1 [assembly: AllowPartiallyTrustedCallers()]
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
In my case, I’m always looking at a single list, so my workflow action only accepts a ListItemID. You’ll probably want to make that more generic by adding a few more parameters to your Action. Again, these custom actions are described in plenty of detail in the MSDN article referenced above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<span class="kwrd">public</span> <span class="kwrd">static</span> Hashtable Package(SPUserCodeWorkflowContext context, <span class="kwrd">string</span> ItemID) { Hashtable result = <span class="kwrd">new</span> Hashtable(); <span class="kwrd">try</span> { result[<span class="str">"Result"</span>] = PackageListItem(closingId, context.CurrentWebUrl); } <span class="kwrd">catch</span> (Exception e) { result[<span class="str">"Result"</span>] = <span class="str">"ERROR"</span> + e.ToString(); } <span class="kwrd">return</span> result; } |
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Where the PackageListItem method looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">string</span> PackageListItem(<span class="kwrd">string</span> ItemID, <span class="kwrd">string</span> webUrl) { SortedList<<span class="kwrd">string</span>, <span class="kwrd">byte</span>[]> files = <span class="kwrd">new</span> SortedList<<span class="kwrd">string</span>,<span class="kwrd">byte</span>[]>(); <span class="kwrd">using</span> (SPSite site = <span class="kwrd">new</span> SPSite(webUrl)) { <span class="kwrd">using</span> (SPWeb web = site.OpenWeb()) { <span class="rem">//Find the list item</span> SPListItem item = web.Lists[<span class="str">"List"</span>].GetItemById(<span class="kwrd">int</span>.Parse(ItemID)); <span class="rem">//Create a PDF of the list item data and add it to the </span> <span class="rem">// compressed files collection</span> files.Add(<span class="str">"ListData.pdf"</span>, CreateDocument(item, <span class="str">"Title"</span>)); <span class="rem">//Loop through all list item attachments</span> <span class="rem">// and add them to the compressed file collection</span> <span class="kwrd">foreach</span> (<span class="kwrd">string</span> attachment <span class="kwrd">in</span> item.Attachments) { SPFile attFile = web.GetFile(item.Attachments.UrlPrefix + attachment); files.Add(attachment, attFile.OpenBinary()); } <span class="rem">//Zip everything up and finish!</span> <span class="kwrd">byte</span>[] package = CreateZip(files); files = <span class="kwrd">null</span>; web.AllowUnsafeUpdates = <span class="kwrd">true</span>; <span class="rem">//Add the zipped package to a doc lib</span> SPFile file = web.Folders[<span class="str">"Archive"</span>].Files.Add( <span class="kwrd">string</span>.Format(<span class="str">"({0})_{1}.zip"</span>, ItemID, DateTime.Now.ToString(<span class="str">"yyyy'-'MM'-'dd HH''mm''ss"</span>)), package); web.Update(); file.Update(); web.AllowUnsafeUpdates = <span class="kwrd">false</span>; package = <span class="kwrd">null</span>; <span class="rem">//return the List Item ID of the generated file to the workflow</span> <span class="kwrd">return</span> file.ListItemAllFields[<span class="str">"ID"</span>].ToString(); } } } |
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
And create document, if you are interested in generating PDF’s, looks like this. I do some hackery to leave out boring version and guid fields from the list data – feel free to do something more intelligent:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">byte</span>[] CreateDocument(SPListItem item, <span class="kwrd">string</span> type) { pdfDocument pdf = <span class="kwrd">new</span> pdfDocument(type, <span class="str">"Some Text"</span>); pdfPage page = pdf.addPage(); page.addText(item[<span class="str">"Title"</span>].ToString(), 20, 730, pdf.getFontReference(predefinedFont.csHelvetica), 12); pdfTable table = <span class="kwrd">new</span> pdfTable(pdf, 1, pdfColor.Black); table.borderSize = 1; table.borderColor = pdfColor.Black; table.coordX = 50; table.coordY = 690; table.tableHeader.addColumn(150); table.tableHeader.addColumn(300); <span class="kwrd">foreach</span> (SPField f <span class="kwrd">in</span> item.Fields) { <span class="kwrd">if</span> (f.Type != SPFieldType.Guid && !f.Title.ToLower().Contains(<span class="str">"version"</span>) && !f.Title.Contains(<span class="str">"Title"</span>)) { <span class="kwrd">try</span> { pdfTableRow row = table.createRow(); row[0].addText(f.Title); row[1].addText(item[f.InternalName].ToString()); table.addRow(row); } <span class="kwrd">catch</span> { } } } page.addTable(table); MemoryStream ms = <span class="kwrd">new</span> MemoryStream(); pdf.createPDF(ms); <span class="kwrd">return</span> ms.ToArray(); } |
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The CreateZip method called in package is also very simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">byte</span>[] CreateZip(SortedList<<span class="kwrd">string</span>, <span class="kwrd">byte</span>[]> files) { MemoryStream ms = <span class="kwrd">new</span> MemoryStream(); <span class="kwrd">using</span> (ZipOutputStream s = <span class="kwrd">new</span> ZipOutputStream(ms)) { s.SetLevel(5); <span class="rem">// 0 - store only to 9 - means best compression</span> <span class="kwrd">foreach</span> (<span class="kwrd">string</span> file <span class="kwrd">in</span> files.Keys) { <span class="rem">// Using GetFileName makes the result compatible with XP</span> <span class="rem">// as the resulting path is not absolute.</span> ZipEntry entry = <span class="kwrd">new</span> ZipEntry(file); <span class="rem">// Could also use the last write time or similar for the file.</span> entry.DateTime = DateTime.Now; s.PutNextEntry(entry); s.Write(files[file], 0, files[file].Length); } } <span class="kwrd">return</span> ms.ToArray(); } |
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
This is relatively resource intensive of course, and will use some of your sandboxed resources. It hasn’t been a problem for me, but your mileage may vary.