Thursday, June 25, 2009

Redirect to list item page knowing only item id, list id and web id

Is never to late to learn something new and useful.

My situation was that I had to open a list item page knowing only the ids of the item, the item container (list in my case) and web. Searching on the net I did not found what I needed. This was until a colleague told me about CopyUtil page from SharePoint and indeed I found a very interesting article from Jan Tielen.

http://weblogs.asp.net/jan/archive/2008/02/26/copyutil-aspx-a-little-sharepoint-gem.aspx

Enjoy !

Wednesday, June 17, 2009

Delete list items with Web Services and JQuery

We can delete multiple list items calling UpdateListItems web method from Lists.asmx web service provided by SharePoint. This method can be called using JQuery ajax method.

In my example I want to delete more items from a list knowing the items id’s. In user interface I have a collection of checkboxes with “id” attribute as id of the list item. Using a selector we obtain the collection of checkboxes, and build using the id attribute of checked one’s the batch that we will send to web method. “Tests” is the list name where I want to delete. For each item we have to add a Method with a distinct id.

function DeleteSelected() {
var chkcoll = $("myselector");
var fields = "";
chkcoll.each(function(i){
if($(this).attr('checked') == true)
{
fields += "<Method ID=\""+$(this).attr('id')+"\" Cmd=\"Delete\"><Field Name=\"ID\">" + $(this).attr('id') + "</Field></Method>";
}
});

var batch = "<Batch OnError=\"Continue\">" + fields + "</Batch>";
var soapEnv = BuildSoapEnv(batch, 'Tests');
theAjaxCallForUpdate(soapEnv, onSuccessDelete);
}




function BuildSoapEnv(batch, listname)
{
var soapEnv =
"<?xml version=\"1.0\" encoding=\"utf-8\"?> \
<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" \
xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"> \
<soap:Body> \
<UpdateListItems xmlns=\"http://schemas.microsoft.com/sharepoint/soap/\"> \
<listName>"
+ listname + "</listName> \
<updates> \
"
+ batch + "</updates> \
</UpdateListItems> \
</soap:Body> \
</soap:Envelope>"
;
return soapEnv;
}



To avoid the “The security validation” error thrown by the web service, the SOAPAction header must be provided using beforeSend option.



The functionToCall is another function that you can call when the web method call is completed for some action that have to take place after. Also “success” option can be used and perform actions only if the calling of the web method was successfully.




function theAjaxCallForUpdate(soapEnv, functionToCall)
{
$.ajax({
url: textWebUrl + "/_vti_bin/Lists.asmx",
beforeSend: function(xhr) {
xhr.setRequestHeader("SOAPAction","http://schemas.microsoft.com/sharepoint/soap/UpdateListItems");},
type: "POST",
dataType: "xml",
data: soapEnv,
complete: functionToCall,
contentType: "text/xml; charset=\"utf-8\""
});
}




function onSuccessDelete()
{
// do something
}



The textWebUrl is a variable that contain the web url. We set this from our .cs code. The scripts are stored in a separate folder in _layouts. The entire Jquery script for calling the web service is in MyScript.js file. This file must be copied into _layouts/JQuery folder together with the last version of JQuery.




StringBuilder sbJQ = new StringBuilder();
sbJQ.Append("<script type=\"text/javascript\" src=\"_layouts/JQuery/jquery-1.3.2.min.js\"></script>");
sbJQ.Append("<script type=\"text/javascript\">").Append("var textWebUrl='"+this.Web.Url+"';").Append("</script>");
sbJQ.Append("<script type=\"text/javascript\" src=\"_layouts/JQuery/MyScript.js\" ></script>");

if (!Page.ClientScript.IsClientScriptBlockRegistered("scriptkey"))
{
Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "scriptkey", sbJQ.ToString());
}

Wednesday, May 20, 2009

Different behavior in New and Edit form for a custom field

A request from a client was to create a custom field for a SharePoint list with different behavior in New form than in Edit. I was able to control this using JavaScript and the only thing left to do is to know in which form the field is rendered.

For this I had to override the RenderFieldForInput method provided by BaseFieldControl class and check the SPControlMode.

public class MyCustomField : BaseFieldControl 
{
....

protected override void RenderFieldForInput(HtmlTextWriter output)
{
string pathToScript = String.Empty;
if (this.Field != null) pathToScript = (this.ControlMode == SPControlMode.Edit) ? "scriptforedit.js" : "scriptforother.js";
output.Write("<script type=\"text/javascript\" src=\""+ pathToScript +"\" ></script>");
base.RenderFieldForInput(output);
}

Tuesday, May 12, 2009

Select parents in a TreeView using JQuery

The requirement is to be able to select the parents of a node in a TreeView if a child is selected. Not only the first parent, all parents and to be able to unselect after if we want.

If we look at generated source code we will notice that the parent node is in a div tag. Inside this div are the childrens inside a table/tr/td tags. So we must look for checkboxes inside a table data, table that is nested into a div. Is better to assign an attribute to the tree to be more easy to find. I added  the attribute “cipg” with the value “mpower”. The id of the tree is “theOne”.

The code below solve this requirement. I’m sure this can be improved.

<script type="text/javascript"> 

function applyToParent(parent, treeId)
{
var actualId = treeId+'theOne';
if (parent.attr('id')!= actualId)
{
var parentId = parent.attr('id').replace('Nodes', '');
var theOne = $("div[cipg='mpower'] table tr td input[type='checkbox'][id^='"+parentId+"']");
theOne.attr('checked', true);
applyToParent(GetParent(theOne), treeId);
}
}

function GetParent(theCurrent)
{
return theCurrent.parent().parent().parent().parent().parent();
}

function checkTheParent()
{
if($(this).attr('checked') == true)
{
var theParent = GetParent($(this));
var currentId = $(this).attr('id');
var treeId = currentId.substring(0,currentId.indexOf('theOne'));
applyToParent(theParent, treeId);
}
}

$(document).ready(function(){
$("div[cipg='mpower'] table tr td input[type='checkbox']").bind('click', checkTheParent);
});

</script>

Friday, May 8, 2009

Display item type icon in SPGridView after search

I had a request to display, after a search, an icon to make a visual distinction between types of every item matching the criteria. So after creating the query I had to render the SPGridView that I fill with the results.

First was the problem of obtaining the link to the icon describing the type for each search result and after that the display.

For the display part I add a field to grid using TemplateField:

//add type image column for Type
TemplateField typeCol = new TemplateField();
typeCol.HeaderText = “Type”;
typeCol.ItemTemplate = new ItemType("TheId", theList);
grdMain.Columns.Add(typeCol);





Here the grdMain is the SPGridView object and “TheId” is a managed property mapped to ows_ID crawled property. If you use CAML query you have to get the ID of the object into the TheId column.



Now I had to create the class that helped me to display properly the icon. This class inherit ITemplate. In the “InstantiateIn” method we have to write the following:




public void InstantiateIn(Control container)
{
Image img = new Image();
img.DataBinding += new EventHandler(img_DataBinding);
container.Controls.Add(img);
}



As parameters into the constructor of this class we receive the name of the displayed column and the list containing the searched object. Using this data we can get all needed information. Here is the code:




using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint;

namespace Com.CFG.WebParts.SearchResultsDocs
{
/// <summary>
/// Implements a custom field which serves as a hyperlink
/// </summary>
class ItemType : ITemplate
{
private String _ColumnDisplay;
private SPList _theList;

#region ITemplate Members

public void InstantiateIn(Control container)
{
Image img = new Image();
img.DataBinding += new EventHandler(img_DataBinding);
container.Controls.Add(img);
}

/// <summary>
/// Initializes the hyperlink field
/// </summary>
/// <param name="ColumnDisplay">The name of the column which contains the value to be displayed</param>
public ItemType(String ColumnDisplay, SPList theList)
{
_ColumnDisplay = ColumnDisplay;
_theList = theList;
}

void img_DataBinding(object sender, EventArgs e)
{
Image img = (Image)sender;

SPGridViewRow container = (SPGridViewRow)img.NamingContainer;

//display name
string itemId = (DataBinder.Eval(container.DataItem, _ColumnDisplay)).ToString();
string imageurl = GetTypeIconLink(_theList, Int32.Parse(itemId));

if (imageurl.Length > 0)
{
img.ImageUrl = "/_layouts/images/" + imageurl;
img.Width = Unit.Pixel(16);
img.Visible = true;
}
else
{
img.Visible = false;
}
}

private string GetTypeIconLink(SPList faqList, int itemId)
{
string returnStr = String.Empty;

SPFile afile = faqList.GetItemById(itemId).File;
if (afile != null)
{
returnStr = afile.IconUrl;
}
return returnStr;
}

#endregion
}
}



If we do not use this class and simply want to add this column to the SPGridView using this code:




SPBoundField acol = new SPBoundField();
acol.HeaderText = "Type";
acol.DataField = "TheId";
acol.Visible = true;
grdMain.Columns.Add(acol);





what we will see into the grid is something like



<img src=”/_layouts/images/…” instead of the icon and this not what we want.



As you noticed from the code, the way to get the path to icon is to use the IconUrl property of item attached file:




private string GetTypeIconLink(SPList faqList, int itemId)
{
string returnStr = String.Empty;

SPFile afile = faqList.GetItemById(itemId).File;
if (afile != null)
{
returnStr = afile.IconUrl;
}
return returnStr;
}



Problem solved.

Managed Property Creator tool

In interface everybody knows to create Managed Properties used by MOSS search. Those can also be created programmatically very simple. This tool is created to be used by the admins on deployment time. Using this tool the admins save time and they are sure that all managed properties were created with no error.

This tool use a configuration file describing the managed properties name, description mapped properties etc. This file must be updated before the tool is used. Here is an example of this file look like:

<MappedProperty>
<Name>TheId</Name>
<Description>My first test</Description>
<UseInScopes>yes</UseInScopes>
<DataType>integer</DataType>
<CrawledProperties>
<CrawledProperty>
<Name>ows_ID</Name>
</CrawledProperty>
</CrawledProperties>
</MappedProperty>



This will create a managed property called “TheId” that will be used in scope search and mapped to the “ows_ID” crawled property. Can be zero, one or more crawled property mapped depending on how much are specified inside the CrawledProperties tag.



Do not forget that a property will be crawled only if contain any data.



For using this tool you have to specify two parameters. First is “-c” or “-d” if you want to create or delete. Second is the name of the configuration file.



Having the code bellow you only have to compile and use.




using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.SharePoint;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Search;
using Microsoft.Office.Server.Search.Administration;
using System.Xml;

namespace ManagedPropertyCreator
{
class Program
{
private static ManagedDataType GetManagedDataType(string strDataType)
{
ManagedDataType returnValue = ManagedDataType.Text;
switch (strDataType)
{
case ("binary"):
returnValue = ManagedDataType.Binary;
break;
case ("datetime"):
returnValue = ManagedDataType.DateTime;
break;
case ("decimal"):
returnValue = ManagedDataType.Decimal;
break;
case ("integer"):
returnValue = ManagedDataType.Integer;
break;
case ("yesno"):
returnValue = ManagedDataType.YesNo;
break;
default:
returnValue = ManagedDataType.Text;
break;
}
return returnValue;
}

private static bool GetUsedInScopes(string isused)
{
if (isused.CompareTo("no") == 0) return false;
else return true;
}

private static Category GetSharePointCategory(Schema sspSchema)
{
CategoryCollection categories = sspSchema.AllCategories;
foreach (Category category in categories)
{
if (category.Name.Contains("SharePoint"))
{
return category;
}
}
return null;
}

private static bool GetUsageMode(string usageMode)
{
if (usageMode == "-c") return true;
else return false;
}

static void Main(string[] args)
{
try
{
if (args.Length != 2)
{
Usage();
return;
}

bool isCreating = GetUsageMode(args[0]);

XmlDocument config_file = new XmlDocument();
config_file.Load(args[1]);
Console.WriteLine("Parsing configuration file... ");

// get the site url
string strURL = config_file.SelectSingleNode("/root/SiteName").InnerXml.ToString().Trim();

SearchContext context;
using (SPSite site = new SPSite(strURL))
{
context = SearchContext.GetContext(site);
if (context != null) Console.WriteLine(String.Format("Connected to {0}...", strURL));
else
{
Console.WriteLine(String.Format("Error : Cannot connect to {0} !", strURL));
return;
}
}
Schema sspSchema = new Schema(context);
ManagedPropertyCollection properties = sspSchema.AllManagedProperties;

// get mapped properties nodes
XmlNode nodeMappedProperties = config_file.SelectSingleNode("/root/MappedProperties");
foreach (XmlNode mpNode in nodeMappedProperties.ChildNodes)
{
string mpName = mpNode.SelectSingleNode("Name").InnerXml.ToString().Trim();
string mpDesc = mpNode.SelectSingleNode("Description").InnerXml.ToString().Trim();
string mpUseInScopes = mpNode.SelectSingleNode("UseInScopes").InnerXml.ToString().Trim();
string mpDataType = mpNode.SelectSingleNode("DataType").InnerXml.ToString().Trim();

// here code for create Managed Property
Console.WriteLine("");
Console.Write(String.Format("{0} <{1}> metadata property", (isCreating)?"Create":"Delete", mpName));
if (properties.Contains(mpName))
{
if (isCreating)
{
Console.WriteLine(String.Format("\nINFO: Managed Property with name <{0}> already exists. Will be skipped...", mpName));
continue; // already exist - skip this one
}
}
else
{
if (!isCreating)
{
Console.WriteLine(String.Format("\nINFO: Managed Property with name <{0}> do not exists. Will be skipped...", mpName));
continue; // do not exist - skip this one
}
}

if (isCreating)
{
ManagedProperty newMP = properties.Create(mpName, GetManagedDataType(mpDataType));
newMP.Description = mpDesc;
newMP.EnabledForScoping = GetUsedInScopes(mpUseInScopes);

MappingCollection mappings = new MappingCollection();
XmlNode mpCrawledProperties = mpNode.SelectSingleNode("CrawledProperties");
// code for map with crawled properties
foreach (XmlNode mpCrawledProperty in mpCrawledProperties.ChildNodes)
{
string nameCrawledProperty = mpCrawledProperty.SelectSingleNode("Name").InnerXml.ToString().Trim();

foreach (CrawledProperty property in GetSharePointCategory(sspSchema).GetAllCrawledProperties())
{
if (property.Name.CompareTo(nameCrawledProperty) == 0)
{
mappings.Add(new Mapping(property.Propset, property.Name, property.VariantType, newMP.PID));
}
}
}
newMP.SetMappings(mappings);

// do not forget to update the new created mapped property
newMP.Update();
}
else
{
foreach (ManagedProperty oldMP in properties)
{
if (oldMP.Name == mpName)
{
if (oldMP.DeleteDisallowed)
{
Console.WriteLine("\n DeleteDisallowed enabled for " + mpName + ". Delete failed.");
continue;
}
oldMP.DeleteAllMappings();
oldMP.Delete();
Console.Write("\n"+mpName + " deleted.");
break;
}
}
}
Console.Write("...OK"); Console.WriteLine("");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
Console.WriteLine("\n All done...press any key");Console.ReadKey();
}

}

static void Usage()
{
Console.WriteLine("Managed Property Creator");
Console.WriteLine("Usage: ManagedPropertyCreator.exe <usage_mod> <conf_file>");
Console.WriteLine("<usage_mod> - Specify if the tool is used to create or delete metadata properties. Acceptev values: -c and -d");
Console.WriteLine("<conf_file> - The file that contain the configuration data.");
}

}
}





And yes, the configuration file:




<?xml version="1.0" encoding="utf-8"?>
<!-- SiteName is the name of the site using the SharedServiceProvider -->
<!-- DataType can be : binary, datetime, decimal, integer, text, yesno -->
<!-- UseInScopes can be : yes or no -->
<root>
<SiteName>http://CF316431:555/ssp/admin</SiteName>
<MappedProperties>
<MappedProperty>
<Name>ManagedProperty1</Name>
<Description>My first test</Description>
<UseInScopes>yes</UseInScopes>
<DataType>text</DataType>
<CrawledProperties>
<CrawledProperty>
<Name>ows_FileLeafRef</Name>
</CrawledProperty>
<CrawledProperty>
<Name>ows_LinkedDocumentField</Name>
</CrawledProperty>
</CrawledProperties>
</MappedProperty>
<MappedProperty>
<Name>ManagedProperty2</Name>
<Description>My first test 2</Description>
<UseInScopes>yes</UseInScopes>
<DataType>text</DataType>
<CrawledProperties>
<CrawledProperty>
<Name>ows_FormulaField</Name>
</CrawledProperty>
</CrawledProperties>
</MappedProperty>
</MappedProperties>
</root>



For using you only have to type



ManagedPropertyCreator.exe –c mpc_config.xml

Modify the fields order into New and Edit form of a list

If you have created a list and want for some reasons to change the order of the fields into the New or Edit form you can do this using the interface or at list creation time, using the schema.xml.

Using the interface you have to go to “List Settings” -> “Columns” -> “Column ordering” if your list has not content type enabled or “Settings” -> “Content Types”-> click on used content types -> “Columns” -> “Column order” in the other case.

If you want to do this using schema.xml you have to go in the <ContentTypes> tag and get the ID of the referenced content type. Using this ID go to the part that describe your contenttype and referenced fields. The fields order is decided by declaration order. For example if we have:

<FieldRefs>
<FieldRef ID="{BAA1289C-E3B8-11DD-95C8-C09456D89593}" Name="Field1" DisplayName="Field1" />
<FieldRef ID="{E7CD97AA-E3B8-12DD-861B-5F9656D89593}" Name="Field2" DisplayName="Field2" />
<FieldRef ID="{B2AB2191-2376-4B2C-938A-74AA4D14FF36}" Name="Field3" DisplayName="Field3" />
</FieldRefs>



Then the fields order will be Field1, Field2, Field3. Now change the declaration order and you will see that the fields order will be changed also.



If we do not use content types and still want to customise the fields order seems that a default content type is used and the name for this is “Item”. Since this is a customisation I think you have to declare a new content type and do the steps from above.



Another way can be to create your own customized page for New and Display.

MOSS Search DateTime comparation

Using the MOSS search one of the criteria was to match some items to a DateTime criteria. I have to specify that I was using FullTextSqlQuery and I had to build the query into the code because some other custom property ware added as criteria for search.

Now back to DateTime, all seems to work for dates greater (>) and less (<). My problem was matching on equal criteria. According to MSDN we must not use equal criteria because the way the date is stored into database. They stored with hour, minute, seconds, milliseconds etc. They say that using “=” you will get unpredictable search results.

But since the client want this and we have to make the client happy I had to find a solution to this. Only way to solve the problem was instead of writing

TimeStart = '2009-04-23'

to write

( TimeStart >= '2009-04-22' ) AND (TimeStart < '2009-04-23' )

as matching criteria.

It seems to work in my case.

Monday, April 6, 2009

AddFieldAsXml bug

Yes, there is a bug on this function allowing that the field created using this function to have after creation as InternalName the DisplayName.

This can cause problems when you ranslate your field name into other languages. In this case when you will try to access your field an error will occure.

This bug can be fixed with a small workaround. For this, into our function that create the field we have to get the InternalName , create the field and after that replace the InternalName of the new created field with the one that we saved before. This solution can be used together with a solution presented into a previous post.

private void SetTheProperGUID(SPList catList, SPField catField, SPList pageLibList) 
{
string fieldXml = catField.SchemaXml;
string strInternalName = catField.InternalName;
string strDisplayName = catField.Title;
Regex regex = new Regex(@"List=\""(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}\""");
if (regex.IsMatch(fieldXml))
{
fieldXml = regex.Replace(fieldXml, "
List=\"" + catList.ID.ToString("B").ToLower() + "\"");
}
// a SharePoint bug force us to make a workaround
// DisplayName must be the same as InternalName at creation
string oldValue = "DisplayName=\"" + strDisplayName + "\"";
string newValue = "DisplayName=\"" + strInternalName + "\"";
fieldXml = fieldXml.Replace(oldValue, newValue);
catField.Delete();
pageLibList.Fields.AddFieldAsXml(fieldXml);
// now the DisplayName must be set with the proper value
SPField fixedField = pageLibList.Fields[strInternalName];
fixedField.Title = strDisplayName;
fixedField.Update();
}

Single check on a TreeView

In the past I showed how to bind a TreeView control to a xml data source. Now the task is to allow selection of none or only one node from this control.

An ideea is to implement this using JQuery. A friend introduced me to this scripting language and now the solution seems simple. I’m sure that it can be improved but is my first contact with JQuery so have mercy.

The only thing that we have to do is to add some new code to the existing TreeView solution.

To display all checkboxes for the tree in order to be able to do the selection we have to set the ShowCheckBoxes property.

TreeView tv = (TreeView)Page.FindControl("ChapterTree");
tv.ShowCheckBoxes = TreeNodeTypes.All;



And this are all modification that we must to into the .cs file. Now we must open the .aspx page and add some lines. Of course  the first that we have to add is the reference to the jquery.js file. You can download this file from the JQuery site and have the latest version.



To be able to access our TreeView checkboxes we have first to take a look into the source code of the page. We will see that our controls have a generated ID. What we can do to access our control is to attach an attribute and search by this attribute. We must notice that our checkboxes are created into some cells of a html table. To access  them we have to create a selector that go from the TreeView control to our checkboxes.




$("div[myAttribute='myValue'] table tr td input[type='checkbox']")



After that we have to bind a function to each of our checkboxes. In this function we unselect all other and select the current node.



Below is all the code that must be added into the head tag:




<script type="text/javascript" src="jquery.js" ></script>
<script type="text/javascript">
function aClick() {
var allNodes = $("div[myAttribute='myValue'] table tr td input[type='checkbox']");
allNodes.bind('click', aClick);
allNodes.attr('checked', false);
$(this).attr('checked', true);
$(this).unbind('click', aClick);
}

$(document).ready(function(){
$("div[myAttribute='myValue'] table tr td input[type='checkbox']").bind('click', aClick);
});
</script>



Now all seems to be simple. From today I start to apreciate JQuery.



Have a nice one !

Wednesday, February 4, 2009

Binding TreeView to Xml

If we want to display hierarchical data we have the possibility to use binding to an asp TreeView control.

To do this all we need to do is to set the DataSource property of the TreeView control to the data source, and then call the DataBind method. In our case the data source is a xml file that can look like this:

treeviewxml 

The result that we want should look like this :

treeviewxmlresult

To obtain this result we first have to add the ASP control to page and after that to add the code for data binding. Here is the html part for adding the control. I added an label also to display messages:

<body> 
<form id="form1" runat="server">
<div>
<asp:Label ID="ChapterLabel" Text="A tree for car classification:" runat="server" ></asp:Label>
<asp:TreeView ID="ChapterTree" runat="server" ShowLines="true" >
</asp:TreeView>
</div>
</form>
</body>



To obtain the desired result as you see we have to create TreeNodeBinding objects that define the relation between data item and the node that it is binding to. Below is the code that do the job:




protected void Page_Load(object sender, EventArgs e) 
{
TreeView tv = (TreeView)Page.FindControl("ChapterTree");

// build the data source for the tree
XmlDocument theBigTree = BuildXmlTree();
XmlDataSource xmlds = new XmlDataSource();
xmlds.Data = theBigTree.InnerXml;
xmlds.DataBind();
TreeNodeBinding treeBinding = new TreeNodeBinding();
treeBinding.DataMember = "tree";
treeBinding.TextField = "name";
TreeNodeBinding nameBinding = new TreeNodeBinding();
nameBinding.DataMember = "node";
nameBinding.TextField = "name";
tv.DataBindings.Add(treeBinding);
tv.DataBindings.Add(nameBinding);
tv.DataSource = xmlds;
tv.DataBind();
}