Showing posts with label sharepoint. Show all posts
Showing posts with label sharepoint. Show all posts

Monday, June 9, 2008

Be careful using non-alphanumeric field names in SharePoint

I found that there are some inconsistency in internal name generating in WSS 3.0. I encountered with it when receiving en error on checking in a file. SharePoint log file did not reveal the problem as usual :) and I used my custom code to troubleshoot. This is the exception description and a stack trace I received:

at Microsoft.SharePoint.SPFieldCollection.GetFieldByInternalName(String strName, Boolean bThrowException)
at Microsoft.SharePoint.SPFieldCollection.GetFieldByInternalName(String strName)
at Microsoft.SharePoint.SPListItem.get_MissingRequiredFields()
at Microsoft.SharePoint.ApplicationPages.Checkin.OnLoad(EventArgs e)

I started to review code in Reflector and discovered that SPListItem.MissingRequiredFields property is checked on page check in. For this purpose it uses SPFieldLink.Name from SPListItem.ContentType.FieldLinks collection to retrieve SPField object from SPListItem.ParentList.Fields collection for every field which is SPFieldLink.Required=true. In order to get SPField object SPFieldCollection.GetFieldByInternalName() method is used and if SPFieldLink.Name field value is not found as internal name of any SPField then we receive "Value does not fall within the expected range" exception.

In my case I have a field named "Description of Change/New document" which is translated to internal name as "Description_x0020_of_x0020_Change_x002F_New_x0020_Document" but for some reason SPFieldLink.Name field value for this field is "Description_x0020_of_x0020_Change_x002f_New_x0020_Document". Feel the difference in unicode of symbol "/": x002f (small 'f') and x002F (capital 'F'). That gives me two different internal names and I have these troubles. I guess there is inconsistency and there are several functions which generate internal name and these functions produce different result in some cases.

When I recreated my field the problem was fixed for which I do not have any explanation.

Friday, May 11, 2007

Yet another scripting host - MyScriptHost

Developing for SharePoint I found myself in situation when I had an access the the server (direct or remote desktop) and need to run some ad-hoc code on it but the server did not have Visual Studio or any other development environment installed. I think this is familiar to some of us. These are some possible options:

  • install Visual Studio or any other development environment (very unlikely that administrator will allow you to do it for test or production environment)
  • develop the code on the other machine and run on the server (good approach but you loose code flexibility especially if you want to write and test your code interactively)
  • use SnippetCompiler. This is a very good tool and I would choose it if it supported command line execution of C# files.
  • use Windows Scripting Host (WSH) to create and run scripts (good approach but you need to use JScript of VBScript)
  • use  Dot Net Script (good tool, but it uses its own file format and does not have IDE)

I wanted to have some kind of a combination of SnippetCompiler and Dot Net Script where I could create some scripts and maybe execute them automatically in future. 

So meet MyScriptHost.

This is a console application with a windowed code editor. It works with one file at a time which can be executed with or without code editor. It can associate .mysh files to itself (I have taken this idea from Dot Net Script project).

Command line usage:

MyScriptHost.exe <filename> <command>

where <filename> is a name of a file and <command> is one of the following:

/open open the file for editing. This is a default behavior if no other command is specified.
/compile compile the file without execution
/run compile and execute the file
/runandwait compile and execute the file and keep the console window open after execution

Code file should be a C# class containing public static void Main() as entry point. The only addition to pure C# code is a metatag <@ Reference Name="assembly name or path" %> in order to define external references. Code file can have any number of these tags at the beginning of the file.

MyScriptHost contains internal static class MyshDebug which has some methods useful for debugging:

static void Watch(params object[] values) Opens a watch window for specified values
static void Print(object obj)

static void Print(object obj, int levels)

Prints object properties to console. It also allows to specify a number of levels to print out.

Watch window is a modal window and allows you to see and change properties in PropertyGrid. Invoke tab allows you to print property value with a specified level of details.

 

This is an example of a script I used with MOSS to import news items from xml file to SharePoint document library.

<%@ Reference Name="C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\ISAPI\Microsoft.SharePoint.dll" %>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using System.IO;
using System.Xml;

public class Class1
{
    private const string m_destNewsItemsFolder = @"C:\My Projects\Sharepoint Scripts\ImportNews\Items\";
    private const string m_destNewsAttachmentsFolder = @"C:\My Projects\Sharepoint Scripts\ImportNews\Attachments\";
    private const string m_siteUrl = @"http://localhost:1003/";
    private const string m_prefix = "newPage_";
    private const string m_pageTemplateName = "page1.aspx";
    
    public static void Main()
    {
        SPSite site = new SPSite(m_siteUrl);
        SPWeb web = site.OpenWeb("News");
        //MyshDebug.Watch(web);
        SPFolder pages = web.Folders["Pages"];
        //MyshDebug.Watch(pages);
        SPFile pageTemplate = pages.Files[m_pageTemplateName];
        //MyshDebug.Watch(pageTemplate);
        
        
        // delete old files
        Console.WriteLine("Deleting previous files...");
        List<SPFile> filesToDelete = new List<SPFile>();
        foreach(SPFile file in pages.Files)
        {
            Console.WriteLine(String.Format("{0}", file.Name));
            if(file.Name.StartsWith(m_prefix))
            {
                filesToDelete.Add(file);
            }
        }
        foreach(SPFile file in filesToDelete)
        {
            Console.WriteLine("Deleting " + file.Name);
            file.Delete();
        }
        

        string[] fileNames = Directory.GetFiles(m_destNewsItemsFolder);

        // Create new files first
        for (int i = 0; i < fileNames.Length; i++)
        {
            string fileName = Path.GetFileNameWithoutExtension(fileNames[i]);
            fileName = m_prefix + fileName;
            //MyshDebug.Watch(fileName);
            Console.WriteLine("Creating " + fileName);
            pageTemplate.CopyTo(pages.Url + "/" + fileName + ".aspx"true);
        }

        // This is to refresh file list
        pages = web.Folders["Pages"];

        // Upload news content to already created files
        for (int i = 0; i < fileNames.Length; i++)
        {
            string fileName = Path.GetFileNameWithoutExtension(fileNames[i]);
            fileName = m_prefix + fileName;
            //MyshDebug.Watch(fileName);
            Console.WriteLine("Uploading " + fileName);

            XmlDocument doc = new XmlDocument();
            doc.Load(fileNames[i]);

            string body = GetNodeText(doc, "//body");
            string date = GetNodeText(doc, "//date");
            string sourceName = GetNodeText(doc, "//sourceName");
            string newsType = GetNodeText(doc, "//newsType");
            string title = GetNodeText(doc, "//title");
            string[] attachments = GetNodeTextArray(doc, "//attachment");

            string creator = GetNodeText(doc, "//creator");
            string creatorName = GetNodeText(doc, "//creatorName");
            string author = GetNodeText(doc, "//author");
            string entity = GetNodeText(doc, "//Entity"); //????
            
            SPFile newPage = pages.Files[fileName + ".aspx"];
            
            
            // adjust body
            body = "<p>" + body.Replace("\r\n""</p><p>") + "</p>";
            //MyshDebug.Watch(body);
            //break;
            
            newPage.CheckOut();
            SPListItem item = newPage.Item;
            item["Page Content"] = body;
            item["Article Date"] = date;
            //item["NewsType"] = newsType;
            item["Title"] = title;
            item["Contact Name"] = author;
            item.Update();
            newPage.CheckIn("");
            newPage.Publish("");
            //newPage.Versions.DeleteAllMinorVersions();
            //break;
        }
    }
    
    private static string GetNodeText(XmlNode root, string xpath)
    {
        XmlNode node = root.SelectSingleNode(xpath);
        if (node == null)
        {
            return "";
        }
        return node.InnerText;
    }

    private static string[] GetNodeTextArray(XmlNode root, string xpath)
    {
        XmlNodeList nodes = root.SelectNodes(xpath);
        List<string> values = new List<string>();
        foreach (XmlNode node in nodes)
        {
            values.Add(node.InnerText);
        }
        return values.ToArray();
    }
    
}
I am not going to explain how this script works. It just demonstrates the power and flexibility I gained in development for SharePoint in restricted environment. I started this project for fun and now it becomes a very useful tool for me.

Thursday, March 8, 2007

Video Tutorials on SharePoint from LearnVu.com

SharePointHosting.com provides on-demand access to SharePoint tutorials from LearnVu to all our SharePoint hosting customers. This is a significant value-add service to our SharePoint hosting offerings. Check it out here

Wednesday, February 14, 2007

How to disable fields on EditForm.aspx. WSS 3.0 version.

I had a task to disable or make read-only some of a SharePoint list fields. I did not wanted to do much development and found some very good solutions for WSS 2.0: How can I make a field read-only? How can JavaScript’ers create custom EditForm without understanding CAML and .NET !

But in case with WSS3.0 these solutions do not work properly because it is very hard to calculate edit element name or id and in my solution I use title attribute of input or select elements generated by ListFormWebPart web part:

<SELECT 
id=ctl00_m_g_4cfb4dc8_9323_4849_bbfb_95af4cc4f470_ctl00_ctl02_ctl00_ctl01_ctl00_ctl00_ctl02_ctl00_ctl00_ctl04_ctl00_Lookup 
title=Company 
name=ctl00$m$g_4cfb4dc8_9323_4849_bbfb_95af4cc4f470$ctl00$ctl02$ctl00$ctl01$ctl00$ctl00$ctl02$ctl00$ctl00$ctl04$ctl00$Lookup> 
<OPTION value=0 selected>(None)</OPTION> 
<OPTION value=6>Transfield Services</OPTION>
</SELECT>

So I created a JavaScript function and placed it after the ListFormWebPart:

<SCRIPT language=javascript>
/// <summary>
/// Disables EditForm field for the specified column title
/// </summary>
function DisableField(title)
{
    for(var i = 0; i < document.all.length; i++)
    {
        var el = document.all[i];
        // find html element with specified title
        if(el.title == title)
        {
            el.disabled = true; // disable
            break;
        }
    }
}
DisableField("Company");
DisableField("Site/Industry");
</SCRIPT>

And these two fields are disabled for editing.

But this ListFormWebPart web part is a tricky thing and creates dropdown lists in different way depending on amount of their items:

Drop down list 1:

<SELECT 
id=ctl00_m_g_4cfb4dc8_9323_4849_bbfb_95af4cc4f470_ctl00_ctl02_ctl00_ctl01_ctl00_ctl00_ctl02_ctl00_ctl00_ctl04_ctl00_Lookup 
title=Company 
name=ctl00$m$g_4cfb4dc8_9323_4849_bbfb_95af4cc4f470$ctl00$ctl02$ctl00$ctl01$ctl00$ctl00$ctl02$ctl00$ctl00$ctl04$ctl00$Lookup> 
<OPTION value=0 selected>(None)</OPTION> 
<OPTION value=6>Transfield Services</OPTION>
</SELECT>

Drop down list 2:

<INPUT class=ms-lookuptypeintextbox 
onkeypress=HandleChar() 
id=Text1 
onkeydown=HandleKey() title=Site/Industry 
onfocusout=HandleLoseFocus() 
onchange=HandleChange() 
value="Transfield Services Generic Documents" 
name=ctl00$m$g_4cfb4dc8_9323_4849_bbfb_95af4cc4f470$ctl00$ctl02$ctl00$ctl01$ctl00$ctl00$ctl03$ctl00$ctl00$ctl04$ctl00$ctl01 
choices="ACT Housing TFM Contracts232ADI Alliance - FFG Refit &amp; Upgrade Project6...<here goes a very long list of choices>" 
match="" optHid="SPSite_x002F_Industry_Hidden" 
opt="_Select">
<IMG 
style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; VERTICAL-ALIGN: middle; BORDER-RIGHT-WIDTH: 0px" 
onclick="ShowDropdown('ctl00_m_g_4cfb4dc8_9323_4849_bbfb_95af4cc4f470_ctl00_ctl02_ctl00_ctl01_ctl00_ctl00_ctl03_ctl00_ctl00_ctl04_ctl00_ctl01');" 
alt="Display lookup values" 
src="INFORM%20Documents%20-%20TMB-0000-SA-9898_files/dropdown.gif">

Thus dropdown list 2 is made of text box and image button which should be disabled as well.

This image button can be found by control id of a text box mentioned in onclick event handler which uses this id in ShowDropdown function call. So this button can be found by content of onclick attribute. Since collection of elements in document.all places elements in order of their declaration in document then we do not need to search all this image button in all the elements but instead of this can check the next element after the have just found element.

Disable field javascript version 2:

<SCRIPT language=javascript>
/// <summary>
/// Disables EditForm field for the specified column title
/// </summary>
function DisableField(title)
{
    for(var i = 0; i < document.all.length; i++)
    {
        var el = document.all[i];
        // find html element with specified title
        if(el.title == title)
        {
            el.disabled = true; // disable
 
            // if the next element has a reference to the current element
            // then disable if as well
            if(i < document.all.length - 1)
            {
                var el2 = document.all[i + 1];
                if(el2.outerHTML.indexOf(el.id) > 0)
                {
                    el2.disabled = true;
                }
            }
            break;
 
        }
    }
}
DisableField("Company");
DisableField("Site/Industry");
</SCRIPT>