Saturday, December 31, 2011

Modifying Leading and Trailing HTML in SPLongOperation

Long running operations show those nice screens in SharePoint telling the user that something that takes a lot of time is being executed and they have to be patient. These screens can be customized with such a title and a subtitle (LeadingHTML and TrailingHTML) which can be set only once for each operation. But what if I have several steps inside the same long operation, and I want to modify these texts and show the user each of the steps?

I took the idea of my solution from James Boman’s blog, and with a very few modifications I got it working for SP2010. The goal is to create a new custom SPLongOperationEx class that will modify directly the HTML code of the long running operation screen via Javascript. This will override the important methods described in the MSDN page, so we can keep the functionality in our new class.

In the constructor, we add the following javascript code to modifiy in the client-side the text.

<script language='javascript'> 
    var spnTarget = document.getElementById('[Leading or TrailingHTML]')
    spnTarget.innerHTML = \"[Text to change]"\
</script>

Probably you will have now the idea of what you have to do, right? The new class will look then as follows:


public class SPLongOperationEx : IDisposable
{
#region Storage

private SPLongOperation mobjSPLongOperation;
private System.Web.UI.Page mobjPage;
private bool mblnBegun = false;
private string mstrChangeScript = "";
private bool disposedValue = false;
#endregion

#region Properties

public string LeadingHTML
{
get
{
return mobjSPLongOperation.LeadingHTML;
}
set
{
mobjSPLongOperation.LeadingHTML = "" + value + "";
if (mblnBegun)
{
mobjPage.Response.Write(string.Format(mstrChangeScript, "spnLeading", value));
mobjPage.Response.Flush();
}
}
}

public string TrailingHTML
{
get
{
return mobjSPLongOperation.TrailingHTML;
}
set
{
mobjSPLongOperation.TrailingHTML = "" + value + "";
if (mblnBegun)
{
mobjPage.Response.Write(String.Format(mstrChangeScript, "spnTrailing", value));
mobjPage.Response.Flush();
}
}
}

#endregion

#region Constructor

public SPLongOperationEx(System.Web.UI.Page page)
{
mobjPage = page;
mobjSPLongOperation = new SPLongOperation(page);
}

#endregion

#region Public Members

public void Begin()
{
mblnBegun = true;
mobjSPLongOperation.Begin();
}

public void End(string vstrRedirectPage)
{
mobjSPLongOperation.End(vstrRedirectPage);
}

public void End(string vstrProposedRedirect, Microsoft.SharePoint.Utilities.SPRedirectFlags rgfRedirect, System.Web.HttpContext context, string queryString)
{
mobjSPLongOperation.End(vstrProposedRedirect, rgfRedirect, context, queryString);
}

#endregion

#region IDisposable

protected void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
this.mobjSPLongOperation.Dispose();
}
}
this.disposedValue = true;
}

void IDisposable.Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

#endregion
}

NOTE: Although it works, I am still having the issue that it is required to duplicate the calling to the setter, because if the new text is set only once, it does not work properly. I guess it is something related to the life-cycle between server and client side… tough stuff anyway.

Tuesday, November 15, 2011

Configure People Search in SharePoint 2010 using https

I have made this configuration a lot of times, but last week I had problem that I could  solved thanks to this blog.

To configure People Search in SP2010, we all know that once you have your one Enterprise Search Site set up, you need to configure the scope and add the MySite url address to the crawling configuration. So go to CA –> General Application Settings –> Farm Search Administration –> Search Service Administration –> Content Sources and select your content source (normally Local Sharepoint sites). Here you need to add the MySite web application to the Start Addresses, but using the sps3 protocol, not the http.

image

I did realize that my User Profile application was running under HTTPS, but I thought that the sps3 protocol should work anyway. Well, that is NOT correct. If your User Profile web application uses HTTPS protocol, you need to add the sps3s to the Start Addresses configuration.

After that, check that the crawling account has access to the User Profile store checking the option in the Administrators configuration of the User Profile Service.

image

The People Search should work now.

Monday, October 31, 2011

Send a SharePoint 2010 document to an external web service using Records Center (II)

In the previous entry I explained the solution adopted to send documents from a SharePoint library to an external web service using Records Center, and the first of the 2 steps to do that:

  • Add a a new connection in the configuration in Central Admin –> General Application Settings –> Configure send to connections.
  • Create a new webservice in a different Web Application (normal ASP.NET service).

In this new post I will explain how to override the standard service OfficialFile.asmx to copy documents to the Records Center, with new functionality to send to our own external service. Following one of the steps that Wictor Wilén described in this post, I managed to implement this same MOSS web service.

First of all, create a new VS2010 project of type ASP.NET Web Service application.

image

Then, using the tool wsdl.exe from the .NET Framework SDK we can get an interface based on the WSDL definition of the OfficialFile.asmx service. So to ensure that we have the latest web service interface, let’s execute the following command to get the interface we will add to our code.

wsdl /out:IRecordsRepositorySoap.cs /n:TestRecordsCenter_WebRole /serverinterface 
http://<serverUrl>/<recordsCenterUrl>/_vti_bin/OfficialFile.asmx?WSDL

That command will create the file IRecordsRepositorySoap.cs containing the definition of the web service. Someting with the following structure:


/// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "2.0.50727.3038")]
    [System.Web.Services.WebServiceBindingAttribute(Name="RecordsRepositorySoap", Namespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/")]
    public interface IRecordsRepositorySoap {
       
        /// <remarks/>
        [System.Web.Services.WebMethodAttribute()]
        [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://schemas.microsoft.com/sharepoint/soap/recordsrepository/SubmitFile", RequestNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", ResponseNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
        string SubmitFile([System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] fileToSubmit, [System.Xml.Serialization.XmlArrayItemAttribute(IsNullable=false)] RecordsRepositoryProperty[] properties, string recordRouting, string sourceUrl, string userName);
       
        /// <remarks/>
        [System.Web.Services.WebMethodAttribute()]
        [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://schemas.microsoft.com/sharepoint/soap/recordsrepository/GetFinalRoutingDes" +
            "tinationFolderUrl", RequestNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", ResponseNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
        DocumentRoutingResult GetFinalRoutingDestinationFolderUrl([System.Xml.Serialization.XmlArrayItemAttribute(IsNullable=false)] RecordsRepositoryProperty[] properties, string contentTypeName, string originalSaveLocation);
       
        /// <remarks/>
        [System.Web.Services.WebMethodAttribute()]
        [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://schemas.microsoft.com/sharepoint/soap/recordsrepository/GetServerInfo", RequestNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", ResponseNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
        string GetServerInfo();
       
        /// <remarks/>
        [System.Web.Services.WebMethodAttribute()]
        [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://schemas.microsoft.com/sharepoint/soap/recordsrepository/GetRecordRoutingCo" +
            "llection", RequestNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", ResponseNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
        string GetRecordRoutingCollection();
       
        /// <remarks/>
        [System.Web.Services.WebMethodAttribute()]
        [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://schemas.microsoft.com/sharepoint/soap/recordsrepository/GetRecordRouting", RequestNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", ResponseNamespace="http://schemas.microsoft.com/sharepoint/soap/recordsrepository/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
        string GetRecordRouting(string recordRouting);
    }

Now we can use this interface as a Service Reference, so add this file directly to the solution to implement at least these five methods. Then create a new service .asmx (it is not necessary to call it OfficialFile.asmx) and the final solution will look to something like this:


image


Keep in mind that it is necessary to implement and override the five methods of the Service reference. One important point is that these methods can return anything (“hello World”, i.e., but obviously the connection will not work properly.´), except the method GetServerInfo(), which MUST return a string containing an XML string with the following format:


<ServerInfo><ServerType>Test Server</ServerType><ServerVersion>1.0</ServerVersion></ServerInfo>

Where ServerType and ServerInfo can be anything. No official documentation was found with this info, and I could only find something about that in Wictor’s blog.


The OfficialFile.asmx file will finally look like this (there are a lot of discussions in different forums about the correct way to declare this service, but this should work 100%). Keep in mind that you may need to deploy the built DLL to the GAC.


<%@ WebService Language="C#" Class="OfficialFile, SharePointToHYDMediaService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f16d3448d010e7e6" %>

And the code-behind (except using sentences):


[WebService(Namespace = "http://schemas.microsoft.com/sharepoint/soap/recordsrepository/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class OfficialFile : System.Web.Services.WebService, CustomRecordsCenter_WebRole.IRecordsRepositorySoap
{
   
    public string SubmitFile(byte[] fileToSubmit, RecordsRepositoryProperty[] properties, string recordRouting, string sourceUrl, string userName)
    {
        return "<ResultCode>Hello World</ResultCode>";
    }
    public string GetServerInfo()
    {
        return "<ServerInfo><ServerType>Test Server</ServerType><ServerVersion>1.0</ServerVersion></ServerInfo>";
    }
    public string GetRecordRoutingCollection()
    {
        return "Hello World";
    }
    public string GetRecordRouting(string recordRouting)
    {
        return "Hello World";
    }
    public DocumentRoutingResult GetFinalRoutingDestinationFolderUrl(RecordsRepositoryProperty[] properties, string contentTypeName, string originalSaveLocation)
    {
        return null;
    }
}

The next step consists on deploy the webservice to a different web application that you can create directly in the IIS Manager. This application needs to run under the SharePoint application pool to work properly, so configure this when you create the WebSite clicking the button Select application pool.


image 


Publish your service to the web application, and cross fingers.


image


If everything went fine, you should be able to browse your custom web service.


image


Finally, add the new service (that remember is based on the original OfficialFile.asmx) as a Service Connection as described in the part I of this post. Clicking in the link to test the service, should show a confirmation popup.


image


The custom Service Connection to send documents to an external web service is up and running, and although making a real test will throw an exception (basically because all the “return ‘Hello World’”) a debugger can be attached to the w3wp.exe process to check that it is working with the custom web service.

Monday, October 17, 2011

Send a SharePoint 2010 document to an external web service using Records Center (I)

One of the main demands that a SharePoint customer -that has just jumped into the platform- usually makes is “how can I connect SharePoint to send/receive documents from SAP/Documentum/MyOwnRepository/whatever?”.

To receive documents or just any kind of data with associated metadata and, i.e., upload these items to a List, I would say that one of the best options is to use standard Web Services. If the standard services are not enough, then we can create our own service and then use the Object Model to build that extra-functionality.

To send documents (again, with or without associated metadata), the chosen solution in out last customer was use some Records Management functionality as an intermediate step to send the document to the customer’s system. The Records Center is an Enterprise site template available since SharePoint 2007 which functionality has been slightly modified for 2010. The cool point of this solution is that the document can be sent to the Records Center either using the contextual menu (Send To –> “Name of the external connection”), or with a custom Activity in a workflow (to send batches of documents)

These are the steps to implement this solution:

  • Add a a new connection in the configuration in Central Admin –> General Application Settings –> Configure send to connections.
  • Create a new webservice in a different Web Application (normal ASP.NET service). This will hold the bridge connection between SharePoint and the external service.

So far, it seems a simple and fast to implement solution, but there are a couple of “special issues features” that may give you headaches for days.

In this first post I am going to cover the connection configuration step and just take a look around to the Records Center, to understand how it works, and what are we going to do.

Let’s begin going to Central Admin –> General Application Settings –> Configure send to connections and add a new connection. You will need to use the service OfficialFile.asmx, as this service implements the methods to copy files into the Records Center. You can find thousands of resources about this service, but the official specification is here: http://download.microsoft.com/download/8/5/8/858F2155-D48D-4C68-9205-29460FD7698F/[MS-OFFICIALFILE].pdf

To set up a new connection using this service, the URL should have the format http://<server>/<recordsCenterUrl>/_vti_bin/officialfile.asmx. Notice that you should have created a Records Center site in order to use this functionality.

image

Now you should be able to see a new link in the contextual menu of the documents libraries. This link wil execute the action specified in the connection settings (copy to Records Center, i.e).

image

Click the “Official File” link in the contextual menu will copy the document to the Records Center using the service OfficialFile.asmx, and that is exactly what we want to do with our own service. Now that we –barely- know how it is going to work, we can create our own service based in the OfficialFile.asmx, and override it to provide a connection to the external service. I personally found this solution great because it is going to use a connection to an external system inside SharePoint context. My point: we are gonna have everything under control.

In the next post I will implement the second part of the solution. That is, develope a “Hello World” web service and give a couple of useful tips to make it work.

Thursday, September 29, 2011

SharePoint Timer Job stops at “Initialized” and clear SharePoint Cache

There are a lot of configuration items that SharePoint cache by default: features, solutions and timer jobs among many other stuff. According to Joe Rodgers in this entry, “The config cache is where we cache configuration information (stored in the config database) on each server in the farm. Caching the data on each server prevents us from having to make SQL calls to pull this information from the configuration database. Sometime this data can become corrupted and needs to be cleared out and rebuilt.”. Yeah.

So just imagine, what could happen if you deploy a solution with a custom timer job, and you forgot in the first line of code something like

System.Diagnostics.Debugger.Launch();


Boahhh: no trace, no error, no action, nothing.


Then you will probably need to clear the SharePoint configuration cache (like Joe Rodgers explain) or at least, if you know what is causing your problem, re-install the feature of your TimerJob. That will clear the cache for that feature/timerjob and when you upgrade your solution and install the feature again, SharePoint will cache the right one.

Tuesday, September 13, 2011

Check if a SharePoint user is member of an AD group

There are several ways to get this information:

But the easiest way I found was using the System.DirectoryServices.AccountManagement namespace. Incredible short implementation, best results… so something like this will solve the problem.


using System.DirectoryServices.AccountManagement;

protected bool CurrentUserIsMemberOfGroup(string groupName)
{
string userLogin = SPContext.Current.Web.CurrentUser.LoginName;
// To get the right context, run with elevated privileges
SPSecurity.RunWithElevatedPrivileges(delegate()
{
var principalContext = new PrincipalContext(ContextType.Domain);
var userPrincipal = UserPrincipal.FindByIdentity(principalContext, System.DirectoryServices.AccountManagement.IdentityType.SamAccountName, userLogin);
var group = GroupPrincipal.FindByIdentity(principalContext , groupName);
return userPrincipal.IsMemberOf(group);
});
}

Notice the SPSecurity.RunWithElevatedPrivileges, as it is necessary to get the info from our AD (in case it is not located in the same machine as our beloved SharePoint). Otherwise, you won’t get access to the “ContextType.Domain”.


Hope this helps somebody.


Cheers!