Home > SharePoint > Create a custom web service in SharePoint 2007

Create a custom web service in SharePoint 2007

2011/10/23

In a previous post i had left opened a question about to call a workflow by javascript: the workflow could be changed and then his GUID is changed, so the javascript code which is referring to the Workflow GUID is no more working (imagine a mission critical site and someone under pressure changes a workflow , without remembering to change the javascript code….)
Apart this , could be interesting to develop custom web services for special needs.
So in this post i examine how to create a custom web service, an activity not simple.

In Visual Studio create a new ASP.NET Web Service Application, with name DemoWs (note that i use a <myname>.<customer>.<project> syntax, that should be always used; Acme is a ideal name for a demo)

It is better, if you have a domain .it .com or .net, to prefix all the classes with it (example Net.StudioAlessi.Acme…)


Rename the Service1.asmx as WfDemo.asmx; the App_Data folder can be deleted.
Uncomment [System.Web.Script.Services.ScriptService], we are planning to use Ajax.

Add a reference to the SharePoint dll (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\ISAPI\Microsoft.SharePoint.dll)

At this point our public class is still named Service1 (the source is: public class Service1 : System.Web.Services.WebService) ; rename (with right clic->Refactor->Rename) as SvcDemoWs.

Tipically i don’t delete the default WebMethod HelloWorld: it is useful to verify that the web service basically works.

Add these “using “to the top:

using Microsoft.SharePoint;

using Microsoft.SharePoint.WebControls;

using System.Net;

View the asmx markup (right clic on asmx->view markup) that at this point is

<%@ WebService Language=”C#” CodeBehind=”WfDemo.asmx.cs” Class=”StudioAlessi.Acme.DemoWs.Service1″ %>

Delete the CodeBehind assignation.

Right clic the project, in Properties ->Signing->Sign the assembly, is not strictly requested a password.

At this point we need a Public key Token, from Visual Studio 2008 Tools menu clic on External Tools , define a Get Public Key Token:


The command is C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\sn.exe

Build the project (is needed the dll for to obtain the public key) and then Tools->Get Public Key Token, we should have an output as


Obviously for your local project the token will be different.

Substitute the asmx markup for the Class with the namespace , the namespace with the name of the public class and the token, in our case the .asmx will be changed to

<%@ WebService Language=”C#” Class=”StudioAlessi.Acme.DemoWs.SvcDemoWs, StudioAlessi.Acme.DemoWs, Version=1.0.0.0, Culture=neutral, PublicKeyToken=70b4890b1aae8b29″ %>

Build the project.

If we run the project we should see (the address and port depends from your pc, from Visual studio we are using the web development webserver) the default page for the web service


That should work


Copy the asmx in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS folder

Start a Visual Studio command prompt


Drag & Drop the dll (in this case the one in C:\Work\WebServices\DemoWs\StudioAlessi.Acme.DemoWs\StudioAlessi.Acme.DemoWs\bin) in c:\windows\assembly, iisreset (important, after a drag & drop in c:\windows\assembly !) then launch from the visual studio prompt: disco http://localhost/_layouts/WfDemo.asmx (the name of our asmx)

Note that we are using a _layout address, that works because we have copied the asmx in the _layouts folder.

The output should be


In the layouts folder we will see 3 new files: WfDemo.wsdl WfDemo.disco and results.discomap


Open the .disco and the .wsdl in a text editor, i use Notepad++.

In the .disco substitute the first line (that contains <?xml version=”1.0″ encoding=”utf-8″?>) with

<%@ Page Language="C#" Inherits="System.Web.UI.Page" %> 
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
<%@ Import Namespace="Microsoft.SharePoint.Utilities" %> 
<%@ Import Namespace="Microsoft.SharePoint" %>
<% Response.ContentType = "text/xml"; %> 

In our .disco the default lines are

<contractRef ref=”http://localhost/_layouts/WfDemo.asmx?wsdl&#8221; docRef=http://localhost/_layouts/WfDemo.asmx&#8221; xmlns=”http://schemas.xmlsoap.org/disco/scl/&#8221; />

<soap address=http://localhost/_layouts/WfDemo.asmx&#8221; xmlns:q1=”http://tempuri.org/&#8221; binding=”q1:SvcDemoWsSoap” xmlns=”http://schemas.xmlsoap.org/disco/soap/&#8221; />

<soap address=http://localhost/_layouts/WfDemo.asmx&#8221; xmlns:q2=”http://tempuri.org/&#8221; binding=”q2:SvcDemoWsSoap12″ xmlns=”http://schemas.xmlsoap.org/disco/soap/&#8221; />

The contractRef and soap address must be changed as

<contractRef ref=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request) + “?wsdl”),Response.Output); %> docRef=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> xmlns=”http://schemas.xmlsoap.org/disco/scl/&#8221; />

<soap address=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> xmlns:q1=”http://tempuri.org/&#8221; binding=”q1:SvcDemoWsSoap” xmlns=”http://schemas.xmlsoap.org/disco/soap/&#8221; />

<soap address=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> xmlns:q2=”http://tempuri.org/&#8221; binding=”q2:SvcDemoWsSoap12″ xmlns=”http://schemas.xmlsoap.org/disco/soap/&#8221; />

The first row of .wsdl (that contains <?xml version=”1.0″ encoding=”utf-8″?>) must be changed as done for the .disco.

Change in the WfDemo.wsdl the soap:address and soap12:address that in our case by default is

location=”http://localhost/_layouts/WfDemo.asmx&#8221;

with

location=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> 

So we change from

<wsdl:service name="SvcDemoWs">
<wsdl:port name="SvcDemoWsSoap" binding="tns:SvcDemoWsSoap">
  <soap:address location="http://localhost/_layouts/WfDemo.asmx" />
</wsdl:port>
<wsdl:port name="SvcDemoWsSoap12" binding="tns:SvcDemoWsSoap12">
  <soap12:address location="http://localhost/_layouts/WfDemo.asmx" />
</wsdl:port>
</wsdl:service>

To

<wsdl:service name="SvcDemoWs">
<wsdl:port name="SvcDemoWsSoap" binding="tns:SvcDemoWsSoap">
  <soap:address location=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> />
</wsdl:port>
<wsdl:port name="SvcDemoWsSoap12" binding="tns:SvcDemoWsSoap12">
  <soap12:address location=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> />
</wsdl:port>
</wsdl:service>

Rename both files in .aspx so that your service is discoverable through SharePoint, in practice join the extension to the file name and add extension .aspx

WfDemo.wsdl -> WfDemowsdl.aspx

WfDemo.disco -> WfDemodisco.aspx

Copy the asmx and these 2 files (that is WfDemo.aspx WfDemowsdl.aspx WfDemodisco.aspx ) to the _vti_bin folder (C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\ISAPI); in this folder there is the spdisco.aspx.

Open it and add these two lines:

<contractRef ref=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(spWeb.Url + “/_vti_bin/WfDemo.asmx?wsdl”), Response.Output); %> docRef=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(spWeb.Url + “/_vti_bin/WfDemo.asmx”), Response.Output); %> xmlns=” http://schemas.xmlsoap.org/disco/scl/ ” />

<discoveryRef ref=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(spWeb.Url + “/_vti_bin/WfDemo.asmx?disco”),Response.Output); %> xmlns=”http://schemas.xmlsoap.org/disco/&#8221; />

Save and launch an iisreset

At this point we can do a first verification by creating an windows forms app.

Add a Service Reference-Advanced->Web Reference, we should have our web service in every web application:


Even in other web application, for example for port 20092, we should have the web service.

Adding a reference named localhost we should have this code working

localhost.SvcDemoWs objProxy = new localhost.SvcDemoWs();
objProxy.Credentials = new System.Net.NetworkCredential("administrator", "password", "domain");
// or objProxy.useDefaultCredentials = True;
MessageBox.Show(objProxy.HelloWorld());

If we see an messagebox with HelloWorld ok, the web service basically works.

Now we try to add a method for to obtain the Guid of a workflow, given his name.

Switch back to the source code of the web service, add a using for microsoft workflow and then the requested method.

Our complete code:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Net;
using Microsoft.SharePoint.Workflow;

namespace StudioAlessi.Acme.DemoWs
{
    /// <summary>
    /// Summary description for Service1
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
    [System.Web.Script.Services.ScriptService]
    public class SvcDemoWs : System.Web.Services.WebService
    {

        [WebMethod]
        public string HelloWorld()
        {
            return "Hello World";
        }

        [WebMethod]
        public string GetWfGuidByName(string workFlowName, string listName)
        {
            SPSite objSite = SPContext.Current.Site;
            using (SPWeb objWeb = objSite.OpenWeb())
            {
                SPList objList = objWeb.Lists[listName];
                SPWorkflowAssociation objAssociationTemplate = objList.WorkflowAssociations.GetAssociationByName(workFlowName, new System.Globalization.CultureInfo("en-US"));
                return objAssociationTemplate.Id.ToString();
            }
        }
    }
}

From Visual studio build, uninstall in windows\assembly the old dll (right clic->uninstall) and drag&drop the news dll, iisreset.

Go to C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS , redo from the visual studio prompt disco http://localhost/_layouts/WfDemo.asmx

This operation regenerate WfDemo.wsdl and WfDemo.disco, redo the same already illustrated operations :

WfDemo.wsdl -> WfDemowsdl.aspx

WfDemo.disco -> WfDemodisco.aspx

and modify the code as already explained (this because we have added an method, otherwise we could change only the dll in windows\assembly)

Copy the 3 files in C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\ISAPI , the modification in spdisco.aspx is already done.

IISreset and then we can try our web service from a windows app .

From Visual Studio new windows app, add the web reference using the path where is the Document library using the workflow that we want test, so enter an address like http://srv2008Moss:1010/intranet/Discovering/_layouts/WfDemo.asmx if your Document library is on the web application on port 1010 in the subsite /intranet/discovering

Adding a Web References named WfDemoProxy for the previous address we have this code in a button click handler

private void button2_Click(object sender, EventArgs e)
{
    WfDemoProxy.SvcDemoWs objProxy = new WindowsFormsApplication1.WfDemoProxy.SvcDemoWs();
    objProxy.Url = "http://srv2008Moss:1010/intranet/Discovering/_layouts/WfDemo.asmx";
    objProxy.Credentials = new System.Net.NetworkCredential("administrator", "password", "domain");
    MessageBox.Show(objProxy.GetWfGuidByName("wfCeoDocuments", "CEO Documents"));
}

Running the code we obtain


Ok, is the guid that we see in templateid for the workflow querystring:


In our SharePoint page add an CEWP and add a menu to a Document Library for test, we can run our previous Windows app code having Fiddler active in order to capture the correct SOAP string.

The code:

<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.2.min.js"></script>

<script type="text/javascript">
    function Custom_AddDocLibMenuItems(m, ctx) {
        var strImagePath = ctx.imagesPath + "XLS16.GIF";
        if (currentItemFileUrl == null)
	        currentItemFileUrl = GetAttributeFromItemTable(itemTable, "Url", "ServerUrl");
        var intPos = currentItemFileUrl.indexOf('/CorporateIdentity/');
        var strItemUrl = ctx.HttpRoot + currentItemFileUrl.substring(intPos) ;
        CAMOpt(m, "Read Wf GUID" , "wfGuidByName('wfCeoDocuments','CEO Documents');" , strImagePath);
        // add a separator to the menu
        CAMSep(m);
        return false;
    }

    function wfGuidByName(wfName, listName){
        var soapEnv = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><GetWfGuidByName xmlns="http://tempuri.org/"><workFlowName>' + wfName + '</workFlowName><listName>' + listName + '</listName></GetWfGuidByName></soap:Body></soap:Envelope>';
        $.ajax({
            url: "http://srv2008Moss:1010/intranet/Discovering/_layouts/WfDemo.asmx",
            type: "POST",
            dataType: "xml",
            data: soapEnv,
            complete: wfQueryCompleted,
            contentType: "text/xml; charset=\"utf-8\""
        }); 
    }

    function wfQueryCompleted(xData, status) { 
        if (status == "success"){
           window.alert(xData.responseXML.text);
        }
    }          
</script>

At runtime


And even here we read the guid:


Advertisements
Categories: SharePoint
%d bloggers like this: