Skip to content

Attention Sharepointers: Want to Write?

September 13, 2010

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

Using the MOSS File Browser with the Telerik RadEditor Link Manager

August 19, 2010

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(&quot;LinkURL&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, 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

July 22, 2010

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

July 21, 2010

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

July 7, 2010

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

June 24, 2010

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

June 17, 2010

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:

  1. Determining whether or not the program has reached the first <item>, and only processing data if it has.
  2. 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.
  3. Converting newlines to paragraphs to re-institute some valid HTML.
  4. Ignoring lines that start with “Contact” or “Updated”  because these are included in most articles but should not be in the summaries.
  5. 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: