The web based GUI for MS CRM 4.0 offers essentially the same user interface to edit picklist attribute values as version 3. There's a tidy web form with all the buttons you'd expect in order to manage individual values (add, edit, delete, etc.), but if you want to add a range of values, say copy and paste them from an Excel spreadsheet, you'll be forced either to add them one-at-a-time, or to use one of the tools out there to generate xml to paste into the right place in your entity definition.
There is a very nice web based tool to help you do just that, but unfortunately anyone creating new entities or customizing existing ones will have to be comfortable manually editing those xml entity files - not something everyone may be happy to do.
The screen capture below demonstrates an alternative: customizing the edit attribute form to add an "edit all" button that allows multiple picklist value / label pairs to be edited and copied at once. In this example, I'm setting up an entity to manage NAICS codes. Rather than having to enter the 20 economic sector codes one-by-one, I can simply copy and paste the value / labels pairs from a spreadsheet.
In follow up posts to this one, I'll go over the details of how to set this up. This will involve creating a new aspx page and writing code to do the behind the scenes work.
Monday, January 5, 2009
Monday, December 22, 2008
Get Distinct Lookup Values
Here's another code example using HubKey's Client-side API for SharePoint. In this case 2 lists are created, a document library and a generic list. A lookup field (Subject) is added to the document library which looks up values in the Title column on the second list. The example also demonstrates getting distinct rows (in this case lookup values) by using the DataView.ToTable() method.
using System;using System.Collections.Generic;using System.Text;// using HubKey's Remote SharePoint API instead of the native object model so we can update a remote serverusing HubKey.Web.Services.SharePoint;//using Microsoft.SharePoint;using System.Data;namespace DevHoleDemo{class Program
{static void Main(string[] args)
{ //always use a test server and site with this beta APISPWeb web = new SPSite("http://remoteserver/testsite001/").OpenWeb();
Guid subjectsListId = web.Lists.Add("HubKey Subjects", "A temporary list.", SPListTemplateType.GenericList);
SPList subjectsList = web.Lists[subjectsListId]; // note that using the SPList.Items property here with the HubKey API returns all the items in the list // which can be expensive if the list contains many items already. See the following code example for a // better way to add items to a list specific to the HubKey API: // http://www.hubkey.com/Documents/HubKeySPAPI/html/60759825-9963-20e4-c31e-ae93ea608330.htm SPListItemCollection subjects = subjectsList.Items; SPListItem item1 = subjects.Add();item1["Title"] = "SharePoint";
item1.Update();
SPListItem item2 = subjects.Add();item2["Title"] = "Microsoft CRM";
item2.Update();
SPListItem item3 = subjects.Add();item3["Title"] = "Microsoft CRM";
item3.Update();
Guid booksDocLibId = web.Lists.Add("HubKey Books", "A temporary document library.", SPListTemplateType.DocumentLibrary);
SPList booksDocLib = web.Lists[booksDocLibId];booksDocLib.Fields.AddLookup("Subject", subjectsListId, false);
booksDocLib.Update();
SPFieldLookup subjectLookup = new SPFieldLookup(booksDocLib.Fields, "Subject");
subjectLookup.LookupField = "Title";subjectLookup.Update();
// build the query to get a list of subjects // (pretend we are starting from scratch)SPFieldLookup subjectLookup2 = (SPFieldLookup)booksDocLib.Fields["Subject"];
SPList subjectsList2 = web.Lists[new Guid(subjectLookup2.LookupList)];
SPQuery query = new SPQuery();
query.ViewFields = "<FieldRef Name='" + subjectLookup2.LookupField + "'/>";
// here we use a data table so that we can get distinct titles (subjects) // if you're not woried about distinct values you could ignore this, but it's // also a convenient way to query the list items (e.g. see the RowFilter below) DataTable subjectsTable = subjectsList2.GetItems(query).GetDataTable();DataView dataView = new DataView(subjectsTable);
DataTable groupedSubjectsTable = dataView.ToTable(true, "Title");
SPFieldLookupValue lookupValue = null;
// here we're looping through 2 distinct title rows ('SharePoint' and 'Microsoft CRM')foreach (DataRow row in groupedSubjectsTable.Rows)
{string title = (string)row[0];
// get the ID of the first row that matches the titledataView.RowFilter = "Title = '" + title + "'";
int firstId = (int)dataView.ToTable(true, "ID").Rows[0][0];
// create a new lookup value (we'll use the 2nd row's lookupValue below)lookupValue = new SPFieldLookupValue(string.Format("{0};#{1}", firstId, title));
}
SPFile file = booksDocLib.RootFolder.Files.Add("test.txt", Encoding.ASCII.GetBytes("A test document."));
// update the subject with the 2nd lookup value; file.Item["Subject"] = lookupValue;file.Item.Update();
// clean up - pause here if you want to view the results on the server.subjectsList.Delete();
booksDocLib.Delete();
}
}
}
Wednesday, November 26, 2008
SharePoint API: A Client-side Object Model
One common issue (see question #2) that developers using Microsoft's SharePoint platform are faced with is that the SharePoint object model can only be used when the application is run on a server in the SharePoint farm.
The following is code that demonstrates using HubKey's Client-side API for SharePoint. This API wraps many of SharePoint Server 2007's Web Services (Lists Web Service, Webs Web Service, etc.) to provide a familiar object model that can be used on remote client machines without having to process XML requests and responses. In this case the code from the MSDN article How to: Upload a File to a SharePoint Site from a Local Folder was copied verbatim, and it compiles and runs successfully on a client computer, even though the SharePoint DLLs are not referenced or installed.
Update: You can download a demo copy of the API - click here for information.
Update II: The API now includes a demo application - see this post for details.
The following is code that demonstrates using HubKey's Client-side API for SharePoint. This API wraps many of SharePoint Server 2007's Web Services (Lists Web Service, Webs Web Service, etc.) to provide a familiar object model that can be used on remote client machines without having to process XML requests and responses. In this case the code from the MSDN article How to: Upload a File to a SharePoint Site from a Local Folder was copied verbatim, and it compiles and runs successfully on a client computer, even though the SharePoint DLLs are not referenced or installed.
Update: You can download a demo copy of the API - click here for information.
Update II: The API now includes a demo application - see this post for details.
using System;using System.IO;// Using HubKey.Web.Services.SharePoint in place of Microsoft.SharePointusing HubKey.Web.Services.SharePoint;namespace HubKey.DevelopmentHole{class Program
{static void Main(string[] args)
{Program program = new Program();
program.RunDemo();
}
public void RunDemo()
{string webUrl = "http://remoteserver";
SPSite site = new SPSite(webUrl);
SPWeb web = site.OpenWeb(); //create a new document libraryGuid id = web.Lists.Add("Temp Doc Lib", "A temp document library.", SPListTemplateType.DocumentLibrary);
SPList list = web.Lists[id]; //add a fieldlist.Fields.Add("Upload Source", SPFieldType.Text, false);
list.Update();
string fileUrl = webUrl + "/Temp Doc Lib/New Folder/test.txt";
string localFileName = @"D:\Temp\test.txt";
string localFileContents = "A temp file uploaded by HubKey's SharePoint client-side API.";
File.WriteAllText(localFileName, localFileContents); //Upload the file using the API code from 'How to: Upload a File to a SharePoint Site from a Local Folder' //see http://msdn.microsoft.com/en-us/library/ms454491.aspxUploadFile(localFileName, fileUrl);
//Check out SPFile file = web.GetFile(fileUrl);file.CheckOut();
//update the title and new upload source field by using the file's SPListItem property...file.Item["Title"] = "A temp title";
file.Item["Upload Source"] = "HubKey's SharePoint client-side API.";
file.Item.Update();
//Check in file.CheckIn("Checked in by HubKey's SharePoint client-side API"); //Get the file contents string serverFileContents = web.GetFileAsString(fileUrl);System.Diagnostics.Debug.Assert(string.Equals(localFileContents, serverFileContents));
//Tidy uplist.Delete();
}
public void UploadFile(string srcUrl, string destUrl)
{if (!File.Exists(srcUrl))
{throw new ArgumentException(String.Format("{0} does not exist",
srcUrl), "srcUrl");}
SPWeb site = new SPSite(destUrl).OpenWeb();
FileStream fStream = File.OpenRead(srcUrl);
byte[] contents = new byte[fStream.Length];
fStream.Read(contents, 0, (int)fStream.Length);fStream.Close();
EnsureParentFolder(site, destUrl);
SPFile file = site.Files.Add(destUrl, contents); SPFolder folder = file.ParentFolder;}
public string EnsureParentFolder(SPWeb parentSite, string destinUrl)
{destinUrl = parentSite.GetFile(destinUrl).Url;
int index = destinUrl.LastIndexOf("/");
string parentFolderUrl = string.Empty;
if (index > -1) {parentFolderUrl = destinUrl.Substring(0, index);
SPFolder parentFolder= parentSite.GetFolder(parentFolderUrl);
if (!parentFolder.Exists) { SPFolder currentFolder = parentSite.RootFolder;foreach (string folder in parentFolderUrl.Split('/'))
{currentFolder = currentFolder.SubFolders.Add(folder);
}
}
}
return parentFolderUrl;}
}
}
Tuesday, November 18, 2008
Get a Valid SharePoint File Or Folder Name
If you're programatically uploading files to a SharePoint document library or creating folders, the file or folder name must not contain certain invalid characters. If it does, you'll get an error, for example:
The file or folder name ".." contains invalid characters. Please use a different name. Invalid characters include the following: ~ " # % & * : < > ? / \ { | }. The name cannot begin or end with dot and cannot contains consecutive dots.
File or folder names must also be less than or equal to 128 characters in length. In addition, the total unescaped URL length (less the length of the URI authority e.g. http://localhost:2008/) must not exceed 260 characters.
The following code uses a regular expression to validate a file or folder name before it is uploaded or created (without checking the total URL length):
The file or folder name ".." contains invalid characters. Please use a different name. Invalid characters include the following: ~ " # % & * : < > ? / \ { | }. The name cannot begin or end with dot and cannot contains consecutive dots.
File or folder names must also be less than or equal to 128 characters in length. In addition, the total unescaped URL length (less the length of the URI authority e.g. http://localhost:2008/) must not exceed 260 characters.
The following code uses a regular expression to validate a file or folder name before it is uploaded or created (without checking the total URL length):
using System;using System.Web;using System.Text.RegularExpressions;namespace DevHoleDemo{class Program
{static Regex illegalPathChars = new Regex(@"^\.|[\x00-\x1F,\x7B-\x9F,"",#,%,&,*,/,:,<,>,?,\\]+|(\.\.)+|\.$", RegexOptions.Compiled);
static void Main(string[] args)
{string fileName = "/new%20file.txt";
string validFileName = fileName;if (GetValidFileOrFolderName(ref validFileName))
Console.WriteLine("A valid file or folder name for '{0}' is '{1}'.", fileName, validFileName);
elseConsole.WriteLine("Could not get a valid file or folder name.");
}
public static bool GetValidFileOrFolderName(ref string path)
{return GetValidFileOrFolderName(ref path, '_');
}
public static bool GetValidFileOrFolderName(ref string path, char replacementChar)
{if (path == null)
return false;
path = illegalPathChars.Replace(HttpUtility.UrlDecode(path.Trim()), replacementChar.ToString());
if (path.Length > 128) {path = path.Substring(0, 128);
return GetValidFileOrFolderName(ref path, replacementChar);
}
return path.Length > 0;}
}
}
Thursday, July 3, 2008
Programatically Manage Remote Documents with the FrontPage RPC Library
We've rolled up some of the ideas in previous posts about FrontPage Server Extensions RPC methods into a free CC licensed c# code library.
You can download the source code along with a demo copy of HubKey's SharePoint Client API from HubKey's website - click here for details.
Documentation about the members exposed through the HubKey.Net.FrontPageRPC.WebClient class can be found online here.
Features of the class include:
The following FrontPage Server Extensions RPC Methods are included in the library:
A very brief code example follows - we'll cover an in depth sample application in a future post:
You can download the source code along with a demo copy of HubKey's SharePoint Client API from HubKey's website - click here for details.
Documentation about the members exposed through the HubKey.Net.FrontPageRPC.WebClient class can be found online here.
Features of the class include:
- Upload or download many documents in one web request using the 'get documents' and 'put documents' methods.
- True file streaming (without pre-loading to memory) for faster uploads.
- Asynchronous methods including an asynchronous MoveDocuments method.
- Auto detection of FrontPage dll paths and versions.
The following FrontPage Server Extensions RPC Methods are included in the library:
- checkin document method - remotely check-in a document to source control.
- checkout document method - remotely check-out a document from source control.
- getDocsMetaInfo method - download document metadata to a client machine.
- get document method - remotely download a document with metadata from a web folder or SharePoint document library. Includes an asynchronous method.
- get documents method - programatically download documents with metadata from a web folder or SharePoint document library in one web request. Includes an asynchronous method.
- list documents method - obtains a list of files and folders on the web or SharePoint server. Sorts the results into a tree structure to allow for use in a TreeView control.
- list versions method - retrieves metadata for all the versions of the specified document.
- move document method - programatically move (rename) or copy a file or folder. Allows for moves between webs or even servers by downloading and then uploading the file.
- open service method - list service meta information.
- put document method - programatically upload a file to a SharePoint document library or web server with meta info. Includes an asynchronous method.
- put documents method - remotely upload files to a SharePoint document library or web server with meta info. Includes a method to asynchronously upload large files. Allows true streaming (without loading into memory first) for large file uploads.
- remove documents method - delete files or folders from a SharePoint document library or web server.
- setDocsMetaInfo method - update document meta data from a client computer.
- uncheckout document method - revert a document to the version before it was checked out.
- url to web url method - break a url into its web and web relative components.
A very brief code example follows - we'll cover an in depth sample application in a future post:
using System;using System.Collections.Generic;using System.Text;namespace HubKey.Net.FrontPageRPC{class Program
{static void Main(string[] args)
{WebClient webClient = new WebClient();
//upload a document and check it inMetaInfoCollection meta = new MetaInfoCollection();
meta.Add("vti_title", "My Title");
UploadDocumentResponse uploadDocumentResponse = webClient.UploadDocument("https://localhost/Docs1/test.txt", @"C:\test.txt", meta);
if (uploadDocumentResponse.Document.IsCheckedOut) webClient.CheckInDocument(uploadDocumentResponse.Document, "uploaded via RPC"); //move a document between serversMoveDocumentResponse moveDocumentResponse = webClient.MoveDocument("http://localhost/Docs2/test.txt", "https://remoteserver/Docs2/test.txt");
}
}
}
Labels:
c#,
Front Page Server Extensions,
RPC,
SharePoint
Wednesday, May 21, 2008
Logging Workflow Errors in the Workflow History List
By overriding the HandleFault method and using the SPWorkflowActivationProperties.Workflow.CreateHistoryEvent method, it's possible to write unhandled exceptions (without much coding overhead) to the history log to assist in debugging long running workflows.
The code at the bottom of this post demonstrates the idea. A potential issue is that the description field of the workflow history view displayed at the bottom of the workflow status page will be truncated if it's longer that 255 characters - making it not ideal to show long exception messages and/or stack traces. One solution is to modify the code the WrkStat.aspx page to display a link to the workflow history list item, for example by adding the following code to override the ViewFields used by the idHistoryView ListViewByQuery (near the very bottom of the page), so that a link to the item (in this case a DocIcon field) is displayed. Alternatively, you could code a url into the description, but that would require information about the workflow history item (ID, etc) that's not available by using the CreateHistoryEvent method.
<%
if (this.idHistoryView.Visible == true) {
this.idHistoryView.Query.ViewFields = "<FieldRef Name=\"Occurred\" /><FieldRef Name=\"Event\" /><FieldRef Name=\"User\" /><FieldRef Name=\"DocIcon\" /><FieldRef Name=\"Description\" /><FieldRef Name=\"Outcome\" />";
}
%>
<SharePoint:ListViewByQuery
runat="server" DisableSort="true" DisableFilter="true" ID="idHistoryView" />
...
The code at the bottom of this post demonstrates the idea. A potential issue is that the description field of the workflow history view displayed at the bottom of the workflow status page will be truncated if it's longer that 255 characters - making it not ideal to show long exception messages and/or stack traces. One solution is to modify the code the WrkStat.aspx page to display a link to the workflow history list item, for example by adding the following code to override the ViewFields used by the idHistoryView ListViewByQuery (near the very bottom of the page), so that a link to the item (in this case a DocIcon field) is displayed. Alternatively, you could code a url into the description, but that would require information about the workflow history item (ID, etc) that's not available by using the CreateHistoryEvent method.
<%
if (this.idHistoryView.Visible == true) {
this.idHistoryView.Query.ViewFields = "<FieldRef Name=\"Occurred\" /><FieldRef Name=\"Event\" /><FieldRef Name=\"User\" /><FieldRef Name=\"DocIcon\" /><FieldRef Name=\"Description\" /><FieldRef Name=\"Outcome\" />";
}
%>
<SharePoint:ListViewByQuery
runat="server" DisableSort="true" DisableFilter="true" ID="idHistoryView" />
...
using System;using System.Collections.Generic;using System.Text;using System.Text.RegularExpressions;using System.Workflow.Activities;using System.Workflow.ComponentModel;using Microsoft.SharePoint.Workflow;namespace HubKey.DevelopmentHole{public sealed partial class MyWorkflow: SequentialWorkflowActivity
{ //assign workflowProperties in the OnWorkflowActivated activitypublic SPWorkflowActivationProperties workflowProperties = new SPWorkflowActivationProperties();
protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
{ try {workflowProperties.Workflow.CreateHistoryEvent(
(int)SPWorkflowHistoryEventType.WorkflowError,
0,
workflowProperties.OriginatorUser,
"Nothing good",string.Format("An exception occured during activity {0}. Check the item data for more information.", executionContext.Activity.Name),
Misc.FormatException(exception));}
catch { }return base.HandleFault(executionContext, exception);
}
}
public static class Misc
{static Regex stackRegex = new Regex(@"\r\n at ", RegexOptions.Compiled);
public static string FormatException(Exception ex)
{return FormatException(ex, new StringBuilder(), 0);
}
private static string FormatException(Exception ex, StringBuilder sb, int tab)
{if (ex == null)
return sb.ToString();string sTab = new string('\t', tab);
sb.AppendFormat("{0}Source: {1}\r\n", sTab, ex.Source); sb.AppendFormat("{0}Message: {1}\r\n", sTab, ex.Message); sb.AppendFormat("{0}StackTrace: {1}\r\n", sTab, FormatStackTrace(ex, sTab));sb.AppendLine();
return FormatException(ex.InnerException, sb, ++tab);}
private static string FormatStackTrace(Exception ex, string sTab)
{ string stack = ex.StackTrace;if (stack == null)
return null;
stack = stackRegex.Replace(ex.StackTrace, delegate(Match m)
{return string.Format("\r\n{0} at ", sTab);
});
return stack;}
}
}
Monday, March 17, 2008
Provide Status Updates for Long Running Operation Jobs - Part II
Following on from Part I that showed a simple way to give users some feedback on the progress status of long running jobs - here is the same code, this time running in an .aspx page. To run ASP.NET server side code you'll need to add a PageParserPath node to your web.config as covered here.
As before, make sure that your site collection includes a list named 'Long Running Operation Status' at the root web. This list is created when you install the Office SharePoint Server Publishing Infrastructure feature at the Site Collection Features level.
<%@ Page language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=12.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" meta:progid="SharePoint.WebPartPage.Document" %>
<%@ Assembly Name="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.Publishing" %>
<%@ Import Namespace="Microsoft.SharePoint.Publishing.Internal" %>
<%@ Import Namespace="Microsoft.SharePoint.Utilities" %>
<script runat="server">
const string PROGRESS_PAGE_URL = "/_layouts/LongRunningOperationProgress.aspx";
public void StartJob(object sender, System.EventArgs e)
{
SPWeb web = SPContext.GetContext(this.Context).Web;
LongRunningJob longRunningJob = new LongRunningJob();
longRunningJob.Title = "Demo Long Running Job";
longRunningJob.TotalOperationsToBePerformed = 15;
longRunningJob.RedirectWhenFinished = true;
longRunningJob.NavigateWhenDoneUrl = SPContext.GetContext(this.Context).List.RootFolder.ServerRelativeUrl;
longRunningJob.Start(web);
string url = string.Format("{0}{1}?JobId={2}", web.Url, PROGRESS_PAGE_URL, longRunningJob.JobId);
SPUtility.Redirect(url, SPRedirectFlags.Default, this.Context);
}
class LongRunningJob : LongRunningOperationJob
{
public override void DoWork()
{
for (this.OperationsPerformed = 0; this.OperationsPerformed < this.TotalOperationsToBePerformed; this.OperationsPerformed++)
{
//Do your work here
this.StatusDescription = string.Format("im in ur long running job, doing ur work {0} of {1}...", this.OperationsPerformed, this.TotalOperationsToBePerformed);
this.UpdateStatus();
System.Threading.Thread.Sleep(1000);
}
}
}
</script>
<asp:Content ID="Content1" ContentPlaceHolderId="PlaceHolderMain" runat="server">
<asp:Button runat="server" OnClick="StartJob" Text="Start Job" id="Button1"/>
As before, make sure that your site collection includes a list named 'Long Running Operation Status' at the root web. This list is created when you install the Office SharePoint Server Publishing Infrastructure feature at the Site Collection Features level.
<%@ Page language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=12.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" meta:progid="SharePoint.WebPartPage.Document" %>
<%@ Assembly Name="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.Publishing" %>
<%@ Import Namespace="Microsoft.SharePoint.Publishing.Internal" %>
<%@ Import Namespace="Microsoft.SharePoint.Utilities" %>
<script runat="server">
const string PROGRESS_PAGE_URL = "/_layouts/LongRunningOperationProgress.aspx";
public void StartJob(object sender, System.EventArgs e)
{
SPWeb web = SPContext.GetContext(this.Context).Web;
LongRunningJob longRunningJob = new LongRunningJob();
longRunningJob.Title = "Demo Long Running Job";
longRunningJob.TotalOperationsToBePerformed = 15;
longRunningJob.RedirectWhenFinished = true;
longRunningJob.NavigateWhenDoneUrl = SPContext.GetContext(this.Context).List.RootFolder.ServerRelativeUrl;
longRunningJob.Start(web);
string url = string.Format("{0}{1}?JobId={2}", web.Url, PROGRESS_PAGE_URL, longRunningJob.JobId);
SPUtility.Redirect(url, SPRedirectFlags.Default, this.Context);
}
class LongRunningJob : LongRunningOperationJob
{
public override void DoWork()
{
for (this.OperationsPerformed = 0; this.OperationsPerformed < this.TotalOperationsToBePerformed; this.OperationsPerformed++)
{
//Do your work here
this.StatusDescription = string.Format("im in ur long running job, doing ur work {0} of {1}...", this.OperationsPerformed, this.TotalOperationsToBePerformed);
this.UpdateStatus();
System.Threading.Thread.Sleep(1000);
}
}
}
</script>
<asp:Content ID="Content1" ContentPlaceHolderId="PlaceHolderMain" runat="server">
<asp:Button runat="server" OnClick="StartJob" Text="Start Job" id="Button1"/>
</asp:Content>
Subscribe to:
Posts (Atom)

