Attention Sharepointers: Want to Write?
I’m at college now, so I’m not working with Sharepoint really at all for the time being. I think this blog already has a lot of good information about uncommon Sharepoint use, though, and I’d hate to see it stagnate while I’m gone. If anyone wants to write entries about anything related to Sharepoint, please comment on this post and I’ll add you as an author. The readership has been growing for a while and this blog gets a lot of Google search hits. It would be awesome if people could continue blogging about useful but unfortunately undocumented customizations for Sharepoint because it can be a great resource for the community at large.
Please comment if you have any interest in writing blog entries about Sharepoint! It can be as few or as many as you like, and it can be about anything Sharepoint-related.
Thanks, and keep on truckin’ on the Sharepoint trail.
-Jeff
It’s time once again for a lesson in Telerik customization. Let’s pretend for a moment that we want our users to be able to build links with complex attributes like anchors and mailto: links without actually having to touch the code. That’s easy with the built-in Telerik RadEditor Link Manager. Let’s also pretend that we want users to be able to browse across the entire site for pages and documents to link to if they so desire. That’s easy with the out-of-the-box Sharepoint file browser. Now, what if we want to do these things together? Not quite as easy.
The problem with doing one or the other is that Telerik’s file browser only allows access to predefined folders which creates a lot of maintenance overhead and is just less usable, whereas the Sharepoint link wizard doesn’t have an interface for mailto:, anchors, or really anything else. The solution: use the MOSS file browser with the Telerik Link Manager.
Note: To do this, you first have to be using custom EditorDialogs. These changes are all going to be in the LinkManager.ascx file.
Believe it or not, this can be achieved pretty quickly and painlessly. By viewing the source of the MOSS link builder and by modifying it to change Telerik’s code as little as possible, I came up with this:
<!--link url text box--> <input type="text" id="LinkURL" name="LinkURL" style="width: 212px;" /> <!--end link url text box--> </td> <!--call link browser; the first parameter of WebForm_PostBackOptions must be the name of the link url text box; keep LinkURL--> <td style="padding-left: 4px;"> <asp:Button ID="Button1" runat="server" Text="Browse" OnClientClick="APD_LaunchAssetPickerUseConfigCurrentUrl( 'linkBrowse' ); return false; ;WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("LinkURL", "", true, "", "", false, true))" Width="70" /> </td><!--end call link browser-->
There should already be similar code with the same name attribute and an <asp:Button> element there. Either comment them out or just replace them with this. This code renders a text box and a browse button (the button’s text is defined in the <asp:Button Text=""> attribute) that, when clicked, will call the functions I’m about to describe. I copied and pasted this javascript from the MOSS link builder as well, and changed some variable names to be more friendly.
with(new AssetPickerConfig("linkBrowse"))
{
DefaultAssetLocation='';
CurrentWebBaseUrl='\u002fsitename\u002fsubsitename';
OverrideDialogFeatures='';
OverrideDialogTitle='';
OverrideDialogDesc='';
OverrideDialogImageUrl='';
AssetUrlClientID='LinkURL';
AssetTextClientID='';
UseImageAssetPicker=false;
DefaultToLastUsedLocation=true;
DisplayLookInSection=true;
ReturnCallback = null;}
If you want, you can play around with the variables in there to customize the file browser popup. They’re named pretty clearly so I won’t go into detail about that. The one thing you probably should change is CurrentWebBaseUrl. Change that to whatever site you’d like the browser to open to, and keep in mind that a slash is represented as \u002f.
Ok, if you’re still with me, congratulations; you’re almost done. The last step is to add two script references in the <head></head> of LinkManager.ascx. They’re system scripts that Sharepoint uses for its link manager, so no installation is required. Paste these two lines in the head section:
<script type="text/javascript" src="/_layouts/1033/core.js?rev=XtdKG9EwDUHSo03sNRdLzQ%3D%3D"></script> <script type="text/javascript" src="/_layouts/1033/AssetPickers.js?rev=pwrdokZjl1CAXjNxKmD8Ug%3D%3D"></script>
If you’re successfully completed these steps, try going to the RadEditor and inserting a link. Your new “Browse” button should be there in place of Telerik’s and if you click it, the MOSS file browser should come up! I hope that was clear enough; it’s only three steps, but they’re complicated ones. Happy Sharepointing!
Web Parts: Using Filtered Data with SPList
This may be common knowledge, but up until recently I only knew how to retrieve the entire contents of a list with SPList, sorted by Sharepoint’s default settings with no filter. There’s an easy way to tailor the returned data to your liking, however. First, create a view that has the filter and sort options you want. This is important though: make sure that all three of the key fields (Title, Event, etc.) are displayed in your view. Tick the boxes next to, for example, Title (linked to item with edit menu), Title (linked to item), and Title. Otherwise, the Web Part will throw an exception if you attempt to display that field’s contents. This view shouldn’t be one that people will ever see; it’s just for your Web Part to use. Now, simply use the following code to only retrieve items included in and sorted by that view.
SPList list = web.Lists[listName];
SPView view = list.Views[viewName];
SPListItemCollection collection = list.GetItems(view);
foreach (SPListItem item in collection)
{
// Loop code
}
That’s all there is to it! Another thing, though. You may have noticed in previous posts that I would loop through items like this:
foreach (SPListItem item in list.items) { ... }
I also recently learned that doing that means Sharepoint will make a database call every iteration through the loop. That’s horribly inefficient. By loading the entire returned list into my variable collections, only a single query is executed and the server load is greatly reduced.
Resources I Used:
Deploying Sharepoint Workflows with WSPBuilder
In my last post, I talked about building a workflow, but glossed over the actual deployment process. I actually used a different process than the tutorial. WSPBuilder is a free command-line application that will take your VS2005 project and generate a .wsp file. That’s awesome, because then it’s self-containing and can be easily deployed onto the server. And the whole process is a snap.
After you download and install WSPBuilder, you just need to go to the project’s folder and run it. Open a Command Prompt window and navigate to the project’s directory (By default, C:\Documents and Settings\[User]\My Documents\Visual Studio 2005\Projects\[Project Name]\[Project Name]). Now, you can probably run WSPBuilder.exe without any arguments. They’re described in the readme if you want to look into them, though. Note: WSPBuilder.exe probably isn’t in your path, and unless you’re going to be using it often you probably don’t want to add it there. Make sure you give the full path to WSPBuilder when you run it. It seems like a stupid mistake, but it’s easy to make.
If all goes well, you now have a file called [Project Name].wsp! Copy this file to your server if it’s not there already, and install and deploy it with stsadm. For example:
stsadm -o addsolution -filename UpdateGlossary.wsp stsadm -o deploysolution -name updateglossary.wsp -immediate -force -allowGacDeployment
I used -force because I had to reinstall the feature and Central Administration gave me an error when I tried to deploy it. There you have it! The basics of deploying a Workflow with WSPBuilder.
Making Sharepoint’s Calculated Fields Work for You
Calculated fields can be great in Sharepoint lists. They can quickly determine appropriate values and keep them up to date, reducing overhead and a source of error at the same time. Unfortunately, they have a few limitations, although most of them would only be encountered by those of us who are always pushing Sharepoint’s limits. For example, in the Content Query Web Part, it’s not possible to group by a calculated field. This is because the values are calculated on the fly every time data is requested, and the CQWP’s logic doesn’t allow items to be grouped by a dynamically generated value. In reality, the value isn’t going to change unless the data changes in almost all cases, so it seems like it would make more sense to have a static field containing that calculated value, and simply have Sharepoint recalculate every time the item is modified. Luckily, we can duplicate this behavior using workflows.
Microsoft pushes workflows as ways to move documents around to the appropriate people in an organization so that a prescribed set of steps can be followed. They can do so much more than that, though. Recently, I was working on a glossary feature in Sharepoint, and I wanted to output the entire list of glossary terms with definitions. Naturally, this entails a gigantic list with thousands of items (or at least it needs to be able to scale to that magnitude), so using a single list with a view is not an option given the 2,000 item ceiling, so the Content Query Web Part was the best path to pursue. Because of the volume of data, I didn’t just want to output a list of items, as that would be almost unusable. The approach I wanted to take was to group the items by their first letter. For this, I made a Calculated field called Letter. The formula is =LEFT(Title,1) to get the first letter from the Title field. Great! That wasn’t too bad. But as we know about Sharepoint, nothing is ever simple. It is (of course) not possible to group by this column due to the reason I stated above.
Enter the workflow. You need Visual Studio 2005 for this part. Workflows can be created in Sharepoint Designer 2007, but they have to be tied to a single list which isn’t very functional in this case because there are going to be multiple lists pulling into one view (the whole reason I’m using a CQWP). To create your workflow in VS2005, follow Sahil Malik’s tutorial, starting with the first step. This walkthrough helped me make a very effective workflow having never created one before.
Now, on to what my workflow actually does. It takes that calculated field (Letter) and copies the value to a static text field named GroupLetter, and also converts that letter to its corresponding number (1-26) so that I can filter using < or > later. Here’s the code for my workflow:
// Set the letter from the Letter field to this variable
// Sometimes Sharepoint appends string;# so remove that
// .ToUpper() to have consistent capitalization
string newLetter = workflowProperties.Item["Letter"].ToString().Replace("string;#", String.Empty).ToUpper();
Hashtable nums = new Hashtable(); // Kind of like an array to store values
nums.Add("A", 1);
nums.Add("B", 2);
nums.Add("C", 3);
// [Do this for the entire alphabet]
nums.Add("X", 24);
nums.Add("Y", 25);
nums.Add("Z", 26);
// Set GroupLetter to the newLetter value
workflowProperties.Item["GroupLetter"] = newLetter;
// Set GroupNum to the corresponding number
workflowProperties.Item["GroupNum"] = nums[newLetter];
// Update the value in the list
workflowProperties.Item.Update();
That does exactly what I want it to do, fairly elegantly. To make it work, you need to have a list with all the fields the workflow references; I like to build the list and save it as a template so I can easily create another just like it. The workflow also needs to be deployed, which the tutorial explains.
Once it’s deployed to Sharepoint, you need to add it to the list so it’ll start doing its thing. To do this, open the list in View All Site Content, go to Settings, and click List Settings. Under the Permissions and Management column, choose Workflow settings. On this screen, you’ll see all the workflows (if any) that are currently attached to this list. Click Add a workflow and choose your newly created workflow from the workflow templates. You’ll have to give it a unique name, and at the bottom you can choose how it runs. Because I want it to update every time an item is changed, I selected the bottom two options to start the workflow when an item is changed or a new one is created. You can also allow users to start it manually on items if you like.
Once you finish that step, you’re done! Your workflow will now run how you set it to. I now have a Content Query Web Part displaying my glossary items grouped by letter, and if I want to have a page for A-D, for example, I can simply filter when 0 < GroupNum < 5 and I’ll get just those items. I hope this shed some light on Sharepoint workflows, as they are an incredibly powerful tool to automate list tasks that would otherwise create a great deal of overhead.
Resources I Used:
CAML Queries, Part 3
Just some more CAML queries for your data-gathering pleasure. Even if you don’t use these specific ones (though you might), they might help you out in building your own. Be sure to check out all my posts on CAML, especially Creating Custom Reports with CAML to get you started.
All files created by or last modified by the current user:
Title it what you want, make a description, and leave CAML List Type empty unless you want to filter by data type. Read the entry I linked above to learn how to do that.
<Where>
<Or>
<Eq>
<FieldRef Name="Editor" LookupId="TRUE"/>
<Value Type="int">
<UserID/>
</Value>
</Eq>
<Eq>
<FieldRef Name="Created" LookupId="TRUE"/>
<Value Type="int">
<UserID/>
</Value>
</Eq>
</Or>
</Where>
All pages modified in the last 30 days, sorted by date modified, descending
Set CAML List Type to <Lists ServerTemplate='850' /> to limit the query to just Publishing Site Pages. Note the Ascending="False" syntax for descending sort order.
<OrderBy>
<FieldRef Name="Modified" Ascending="False" />
</OrderBy>
<Where>
<Geq>
<FieldRef Name="Modified" />
<Value Type="DateTime" IncludeTimeValue='TRUE'>
<Today OffsetDays="-30" />
</Value>
</Geq>
</Where>
Unfortunately, it appears that Sharepoint can’t sort by a Choice field in a list, though (at least not out of the box).
Writing a Custom RSS Reader Web Part
For this entry, I’m exploring a very specific case that uses things that are practical in a multitude of situations. Specifically, I’m developing a Web Part that will integrate a user-defined number of stories from a Xigla Absolute News Manager system into a custom Javascript news slideshow of sorts. To do this, I’m going to parse through an RSS file and take out the data I want. I’m then going to convert it to the format the Javascript slideshow wants.
Step 1: Reading RSS
After a little bit of research, this isn’t too hard. In fact, Microsoft Support has an article with everything you need for this, but I’ll share some code and elaborate a bit anyway. In my case, the News Manager can generate an RSS feed with a certain amount of items — perfect! I’m building the RSS URL and storing it to rssURL. Then, I’m loading that RSS file into something usable with System.XML.XMLTextReader. The variable articleCount is a user-defined value representing the number of articles to display.
// Pull in data from RSS feed // ?h is the number of articles to display string rssURL = "http://wwwdev.co.boulder.co.us/newstest/rss.aspx?z=1&h=" + articleCount; XmlTextReader reader = new XmlTextReader(rssURL);
Now, as the Microsoft article says to do, I’m going to loop through a while() statement as such:
while (reader.Read())
{
}
Inside this while() statement, I need to accomplish the following things:
- Determining whether or not the program has reached the first
<item>, and only processing data if it has. - Removing HTML tags from CDATA’d
<description>s because there seems to be lots of Microsoft Word-generated HTML and life will be better without it. - Converting newlines to paragraphs to re-institute some valid HTML.
- Ignoring lines that start with “Contact” or “Updated” because these are included in most articles but should not be in the summaries.
- Limiting the number of sentences displayed to a user-defined value, with sentences being recognized by a period and a space (unless it’s a.m. or p.m.).
Step 2: Format the content
As I’ve mentioned in a previous post, script.Append(); is just adding code to the variable that will be output at the end of my program, so for those of you keeping score at home, you can treat it as Console.Write(); or anything else that outputs text. To strip HTML tags, I’m going to stealuse a regular expression method as described on csharp-online.net (thank you!). This is the code from that website; I adapted it into my while() loop as you’ll see below.
using System.Text.RegularExpressions;
...
const string HTML_TAG_PATTERN = "<.*?>";
static string StripHTML (string inputString)
{
return Regex.Replace
(inputString, HTML_TAG_PATTERN, string.Empty);
}
Step 3: Put it together
And now the whole thing together in my while() loop:
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
// Set thisElement to the name of the current tag we're in
thisElement = reader.Name;
if (thisElement == "item")
{
// We have reached the first <item>; it's ok to process data
reachedContent = true;
}
break;
case XmlNodeType.Text:
if(thisElement == "title" && reachedContent == true)
{
// Article title
script.Append("<p><strong>" + reader.Value + "</strong></p>\n");
}
break;
case XmlNodeType.CDATA:
if (reachedContent == true)
{
// Remove HTML tags:
articleSummary = Regex.Replace(reader.Value,
HTML_TAG_PATTERN,
string.Empty);
// Replace with " "
articleSummary = articleSummary.Replace(" ", " ");
// Split up article by newlines:
String[] articleParts = articleSummary.Split('\n');
int customSummarize = summarize;
foreach(string articleLine in articleParts)
{
// Do first two lines start with "Contact" or "Updated"?
// If not, add the line to articleOutput.
if (articleLine.Length >= 7 && loopcount < 2)
{
string firstSeven = articleLine.ToLower().Substring(0, 7);
if (firstSeven.Contains("contact")
|| firstSeven.Contains("updated"))
customSummarize++;
else
articleProcessed += "<p>" + articleLine + " </p>\n";
}
else if (articleLine.Contains("-END-"))
{
;
}
else
articleProcessed += "<p>" + articleLine + " </p>\n";
loopcount++;
}
// Cut the summary down to customSummarize number of sentences
string[] articleOutputParts = Regex.Split(articleProcessed,
"\\.[\\s]");
// Check if one of the ". " found was actually from a.m. or p.m.
// If so, increase summarize to compensate
for (int i = 0; i < summarize; i++)
{
articleOutput += articleOutputParts[i] + ". ";
if (articleOutputParts[i].Length > 4)
{
string thisSentence =
articleOutputParts[i] + "[end]";
string preceding =
thisSentence.Substring((thisSentence.Length - 8),
3).ToLower();
if (preceding == "a.m" || preceding == "p.m")
summarize++;
}
}
script.Append(articleOutput);
// Reset variables for next time through
articleOutput = "";
articleProcessed = "";
loopcount = 0;
}
break;
}
}
I apologize for the gratuitous line breaks; code doesn’t wrap well. There you have it, though. That code does my five things pretty well, and hopefully you can glean something useful from it, even if it’s just how to use substrings or regular expressions. The next step would be to apply styles to the output; right now it’s a bold title with the content preview in paragraphs. If you have any input on the code, better ways to do things, etc., feel free to comment and help out me and anybody else who happens upon this entry.
Resources I Used:
A Few More CAML Reports
I’ve been writing a few more CAML reports, so here’s some more code and resources to write your own! Remember, queries can’t have any line breaks for Sharepoint reports.
Large File Sizes:
<Where>
<Geq>
<FieldRef Name="File_x0020_Size" />
<Value Type="Lookup">1000000</Value>
</Geq>
</Where>
Large JPG Images: (adapted from the ECM Team blog)
<Where>
<And>
<Eq>
<FieldRef Name="DocIcon" />
<Value Type="Computed">jpg</Value>
</Eq>
<Or>
<Gt>
<FieldRef Name="ImageWidth" />
<Value Type="Integer">200</Value>
</Gt>
<Gt>
<FieldRef Name="ImageHeight" />
<Value Type="Integer">200</Value>
</Gt>
</Or>
</And>
<OrderBy>
<FieldRef Name="FileSizeDisplay" Ascending="False" />
</OrderBy>
</Where>
For many of these CAML queries, you’ll find yourself in need of the internal column names – those are the names Sharepoint uses for columns in lists and libraries. For example, Sharepoint displays “File Size”, but to compare file size data, you need to use the internal value File_x0020_Size. A great list of display names, corresponding internal names, and the type used in the <Value> tag can be found in this MSDN article.
Creating Custom Reports with CAML
Right now, I’m working with the powerful CAML functionality in Sharepoint to generate automatic reports of site data. Personally, I would much rather write SQL queries as they are much more straightforward, but hey, it is the Sharepoint way to do things less efficiently. The first report I wanted to create was called, “New in the Past Week.” It displays items, believe it or not, that have been created in the past week. But first things first. To create (or modify) a report, you need to be in the Content and Structure Reports library, found under the View All Site Content > Content and Structure Reports. Once you’re in that library of sorts, you’ll see several reports that come with Sharepoint out of the box, such as Checked Out To Me and Expiring Within Next Seven Days, few of which you’ve probably ever used. And that’s why we’re creating our own. If you don’t know already, you can view these reports by going to Site Actions > Manage Content and Structure, and choosing a view (as described with screenshots on this MSDN blog entry).
So on to creating my report, “New in the Past Week”. First, click the New button to create a new item. I filled in a name and nice little description; these don’t really matter, they’re just for looks. The first step is the CAML List Type field. This isn’t required, but if you’re doing anything specific at all, you’re going to want to use it. It basically lets you specify what to query through. There are two possible tags you can enter here, <Lists ServerTemplate='' /> and <Lists BaseType='' />. In my case, I want to search through all pages, so I used:
<Lists ServerTemplate='850' />
The 850 tells Sharepoint to search through Publishing Site Pages, which is all I want to look through. For other data types, take a look at this Sharepoint Reference Sheet from Abstract Spaces. It has pretty much everything else. Now, the CAML Query field is where we get down to business. This is where the actual query goes. Long story short, for my purposes, this query works just fine:
<Where>
<Geq>
<FieldRef Name="Created" />
<Value Type="DateTime" IncludeTimeValue='TRUE'>
<Today OffsetDays="-7" />
</Value>
</Geq>
</Where>
Essentially, it’s looking for things whose Created field is greater than or equal to (<Geq>) today’s date minus seven days (<Today OffsetDays="-7" />). Would this have been easier to write in normal SQL? Yes. Does it still work? Definitely. This query combined with my list type specification above results in a view of all pages created in the past week!
As far as writing your own queries, Sharepoint Magazine has a great article about the basic operators in CAML. The syntax is a little strange at first, but it’s easy to learn and there are many helpful examples online.
Resources I Used:
Designing Web Parts: A Quick Reference Sheet
For the past few days, I’ve been working on designing a web part that in essence builds a Coin Slider slideshow given the name of a Picture Library. This has some inherent problems, and the main one I’m dealing with is varying image size. This slideshow script displays each picture as a background image —this means that if the box is too big for the picture, it will tile, and that doesn’t look too pretty. The best dynamic solution I’ve come up with is to set the slideshow size to the dimensions of the smallest image in the library (and hope there aren’t any really weird dimensions like 20×500 that will display wrong). Much to my dismay, several Javascript solutions failed, so I am currently developing logic in the .cs file. That’s easier said than done. If you find yourself in a similar situation dealing with picture info while designing a Web Part, the following should be at least marginally useful to you.
Section 1: Formatting the URL
If you just pull the list name and filename into your Web Part, characters like spaces are going to come over normally, which works in most browsers but it’s better to convert those to %20′s to be safe. C# has a nice little method for doing this:
System.Web.HttpUtility.UrlPathEncode(yourContentHere)
This will turn yourContentHere into something web-friendly that any browser can understand without a problem.
Section 2: Pulling image data
Now that you have the image displaying correctly according to semantics and Internet Explorer (two polar opposites, coincidentally), you might want to pull in image data such as dimensions. For my Web Part, I’m looping through all the items in a given Picture Library with each item named, cleverly, item. To get the width and height, you would do something like this:
script.Append("Image dimensions: " + item["Picture Width"].toString()
+ " x " + item["Picture Height"].toString());
Believe it or not, those are indeed what Sharepoint names the respective values. That code would have the following output for a 900 x 900 picture:
Image dimensions: 900 x 900
Using this information, I can determine the smallest dimensions (by using Area so as to include both width and height) and display my slideshow accordingly. For any other field in the Library, just use item["FieldName"] to refer to it. For example, I have a field called “Alt” for the alternate text attribute of my images. To call them, I just use:
script.Append("<img alt=\""+ item["Alt"].toString() +"\">");
Hopefully this will shine a little bit of light on how to design a Web Part that pulls data from a Library.
Part 3: Using this data
Now, I want to run some basic calculations on the height and width of images. To do this, they need to be of the data type int (or similar). The value returned by item["Picture Height"] is of the data type Object. This is a problem. A problem overcome by the following code:
height = Convert.ToInt32(item["Picture Height"]); width = Convert.ToInt32(item["Picture Width"]);
And now I have my int variables height and width with integer values, ready for whatever math I can throw at them.
Edit: I forgot to include something that might be important. In case you wondered what I was referencing with script, as in script.Append(), it’s defined as such:
System.Text.StringBuilder script = new System.Text.StringBuilder();
Which just means it’s the code my script builds and ultimately outputs. Sorry about any confusion that may have caused.