Welcome to MSDN Blogs Sign in | Join | Help

Michael Washam's Development Blog

SharePoint, .NET, Silverlight, C++ who knows..
How to Deploy a Workflow with VSeWSS 1.3

Visual Studio Extensions 1.3 has new features that allow the packaging and deployment of a workflow into a SharePoint Solutions Package (.WSP).

I’m starting off with a real simple workflow. Essentially, when the workflow is activated it will assign a task to a user in my site. It will then wait for the task status to be completed before ending the workflow. For this exercise it doesn’t matter how complex the workflow is or isn’t.


Project Creation

To get started ensure you are running the 1.3 versions of the VSEWSS extensions.
In your workflow solution right click on your existing workflow solution and select Add -> New Project.
Select Language (C# or VB) -> SharePoint -> Empty

You should end up with something like this:

One of the new features of VSeWSS 1.3 is the ability to deploy additional assemblies (such as workflows) without the need for manually manipulating the manifest and moving around files. This will be our next step.

Add the Workflow Assembly for Deployment

Right click on your newly created Empty SharePoint project and click Add Reference -> Browse and browse to the output folder of your workflow. Select the workflow assembly and press OK.

Once the reference is added you will need to manually change the “Copy to Local” property of the reference to true. This is how VSeWSS identifies assemblies that will be included in the package.

Open up WSP View (View -> Other Windows -> WSP View) then click the Refresh tool bar button and open the manifest.xml.
You should see something very similar to this:

Add a new Feature 

To deploy the WF you will also need to deploy the associated configuration files too. For this we will use another new feature within VSeWSS 1.3. The ability to add a new Site scoped feature with an elements.xml file directly from within WSP View.

Open WSP View and click the Add New Feature button.

Within the Feature Scope Settings dialog change the Feature Scope to Site and check Add default element.xml file.

Rename the Feature

Rename the resulting Feature1 to your desired feature name. Highlight Feature1 and press F2. In this example the resulting feature will be named WFToDeploy. Note, if you do not want to keep the Feature1 naming convention on your file system you will need to rename this directory in Solution Explorer as well.

Expand the feature and open the Feature.xml that was created. Find the Title attribute on the feature element and change it to your feature name.

The next step is to incorporate the configuration in your existing workflow.xml into the feature. 

Combining the Workflow.xml into the Element1.xml

Open the created Element.xml you should have something like this:

Remove the closing slash / on the element and add a closing </Elements> below it. Be careful not to remove the Id attribute!

From Solution Explorer open the workflow.xml file within your existing workflow project. Copy the XML starting at <Workflow and ending at </Workflow> in between the <Elements element in the element1.xml file.

The result should be similar to this:

Custom Forms

If you are also deploying ASP.NET or InfoPath forms along with your workflow you will need to add them as Modules to your WSP project.

Deploying the project

Right click on your project and select Build to ensure everything compiles (fix any errors if present).
You now have the option of packaging or deploying. If you right click in select Package your .WSP will be built into the output directory but not deployed. Clicking Deploy will package the .WSP and also deploy it to the specified SharePoint site (specified under Project Settings -> Debug -> Start Browser with URL).

Testing the Workflow

Right click on your WSP project and click Deploy. Now browse to the SharePoint site you deployed to and go to a document library such as Shared Documents. 

Click Settings -> Document Library Settings -> Workflow Settings -> Add a Workflow.

You should see your workflow in the Select a workflow template list.

Give the workflow a unique name and press OK to associate it.
Back in Shared Documents bring up the actions menu for an existing document (create one if necessary) and click the Workflows item.

Start the Workflow

Select your workflow from the list to start.
You should see a new column added to your library with the same name as your workflow. If it successfully started it will have a value of In Progress or Completed (depending on what it actually does). 

Done!

Why does my event receiver run multiple times?

Why does my event receiver / handler run more than once when I update or add a new list item?

There are a few common situations where this can occur. The first and most obvious is when you are handling an ItemUpdated event and within the event you  update the item in question again from your code.  Obviously, without precautions this will trigger another update! (This can happen from other events too such as ItemAdded)


The following function writes an entry to the Application event log everytime it runs. It also checks for a null title or a title that does not equal "Title Updated in ItemUpdated". If it finds either it updates the Title field of the current item.

public override void ItemUpdated(SPItemEventProperties properties)
{
    LogEvent("In ItemUpdated");
    SPListItem currentItem = properties.ListItem;
    // If title is not what we want set it
    if (currentItem["Title"] == null || currentItem["Title"].ToString().Equals("Title Updated in ItemUpdated") == false)
    {
        currentItem["Title"] = "Title Updated in ItemUpdated";
        currentItem.Update();
    }
}

FYI LogEvent() is just some simple code to log to the application event log:

private void LogEvent(String msg)
{
    SPSecurity.RunWithElevatedPrivileges(delegate()
    {
        System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
        el.Source = "DupeEventReceiver";
        el.Log = "Application";
        el.WriteEntry(msg);
    });
}

 

When I update a field in one of the listitems I see the following logged in the event log (as expected).


The reason it only runs once is because I am checking the title value before I do the update. The 2nd time around the title is the same so the Update() does not occur.

The solution to this problem is very simple: Call DisableEventFiring immediately before you modify the list item and then call EnableEventFiring when you are finished.

public override void ItemUpdated(SPItemEventProperties properties)
{
    LogEvent("In ItemUpdated");
   
    // Prevent update events from going out during the update
    this.DisableEventFiring();

    SPListItem currentItem = properties.ListItem;
    // If title is not what we want set it
    if (currentItem["Title"] == null || currentItem["Title"].ToString().Equals("Title Updated in ItemUpdated") == false)
    {
        currentItem["Title"] = "Title Updated in ItemUpdated";
        currentItem.Update();
    }
    // Re-enable event firing
    this.EnableEventFiring();

}

The second common scenario for your event receiver running multiple times is having the event handler registered to the same list more then once.

I've ran into this scenario before mostly in developer environments where registration is occuring in multiple ways. For example deploying your event receiver with VSEWSS and then programmatically registering it (again) in a feature receiver.

To detect whether this is going on in your environment you can write a very simple console application to enumerate through the registered receivers for your list.

private static void ListEventReceivers(String SiteName, String ListName)
{
    using (SPSite sps = new SPSite(SiteName))
    {
        using (SPWeb spw = sps.OpenWeb())
        {
            SPList splist = spw.Lists[ListName];
            foreach (SPEventReceiverDefinition sprd in splist.EventReceivers)
            {
                Console.WriteLine(sprd.Class + " " + sprd.Name);
            }
        }
    }

The output for my test environment after the vsewss/programmatic mix:

You'll notice from the output that my event receiver has multiple registrations for the ItemUpdated() event. This is because my feature receiver programmatically registered this event.. Likely, you'll only run into this scenario in developer environments where there is a lot of experimenting going on but it is a good thing to be aware of.

The output from my event receiver when updating a list item is as expected:

 

(Note I'm only calling LogEvent in ItemUpdated() not ItemUpdating())

To clean this list up you can programmatically delete the event receivers and re-register.

This example programmatically removes all event receivers from the assembly "BlogSampleDupeER.dll":

private static void UnregisterEventReceiver(String SiteName, String ListName)
{
    using (SPSite sps = new SPSite(SiteName))
    {
        using (SPWeb spw = sps.OpenWeb())
        {
            SPList splist = spw.Lists[ListName];
            List<SPEventReceiverDefinition> ersToRemove = new List<SPEventReceiverDefinition>();

            foreach (SPEventReceiverDefinition sprd in splist.EventReceivers)
            {
                if (sprd.Assembly.Contains("BlogSampleDupeER") == true)
                {

                   ersToRemove.Add(sprd);
                }
            }

            foreach (SPEventReceiverDefinition sprd in ersToRemove)
            {
                sprd.Delete();
            }
        }
    }
}


A third culprit is if you have "Require Checkout" enabled on your document library. To see a workaround for this see the following KB: http://support.microsoft.com/kb/939307

There are likely other causes for event receivers running more than once but these are the two that I run into the most while working with customers.

Hope it helps!

 

How to extract aspx files out of a SharePoint content database.

STSADMExtractFiles is a sample application that extends the STSADM.EXE command line via the ISPStsadmCommand interface and adds three new commands to the tool:  savefile, savefromfolder and restorefile. The purpose of the sample is to show how an application could be built to extract content out of a SharePoint content database to the file system in order for the content (aspx files etc..) to be saved into a source control system.

These commands allow the user to save content from a SharePoint site directly to the file system and restore a file back to the SharePoint.

The sample uses the WebPartPagesWebService (from _vti_bin/WebPartPages.asmx) to retrieve the file content. This is the same web service that SharePoint Designer uses. The sample uses Regular Expressions to remove some of the SPD specific attributers when saving the file. For restoring the file it uses the FrontPage RPC put_document command.

Supported Commands:

The SaveFromFolder command allows you to point to a starting folder and extract all of the files recursively to the file system.

Usage:

stsadm -o savefromfolder
        -weburl <url to SharePoint web>
        -localworkingdirectory <path where content will be saved>
        -starturl <relative url to folder on site where to start save from ( / for root)>
        [-skipcheckedout <skip files that are checked out>]
        [-onlycustomized <only save files that are customized>]
        [-lastmodifieddate <only save files that are modified after this date>]
        [-lastmodifiedby <only save files that are last modified by the specified user (Modified By in SharePoint Designer)>]
        [-nofilecompare <forcibly save files without comparing existing files first>]
        [-fileextension <only save files with the specified file extension>]
        [-skipemptydirs <skip creation of directories that do not contain files.>] 

Example:

The end result of saving a test team site:

 

The SaveFile command allows you to save an individual file.

stsadm -o savefile
        -weburl <url to SharePoint web>
        -localworkingdirectory <path where content will be saved>
        -sourceurl <relative url of source file>
        [-nofilecompare <forcibly save file without comparing existing files first>]

The RestoreFile command allows you to restore a file from the file system back to the SharePoint site. Note - this is not full fidelity, it will not keep individual permissions etc.. it is really only useful for replacing content.

stsadm -o restorefile
        -weburl <url to SharePoint web>
        -localfilepath <path to source file>
        -targeturl <relative url to target file>

 

See the attached sample for the project/source to the sample. The readme.txt has full instructions on how to install the sample.

 

Calling a WCF WebService from a SharePoint WebPart using JavaScript

The following examples demonstrates a JavaScript client calling into a WCF Service which allows updates of SharePoint list data. All deployable from a SharePoint solution (WSP).

The first thing you'll need is the .NET Framework 3.5 installed on your SharePoint server (or VS.NET 2008).

.NET 3.5 Install

http://www.microsoft.com/downloads/details.aspx?FamilyId=333325FD-AE52-4E35-B531-508D977D32A6&displaylang=en

.NET 3.5 SP1 Install

http://www.microsoft.com/downloads/details.aspx?FamilyId=AB99342F-5D1A-413D-8319-81DA479AB0D7&displaylang=en

The second step is to configure the web.config file of your SharePoint site to load the correct assemblies.

In the <configSections> element add the following:

  <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
   <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
    <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
    <sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
     <section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="Everywhere"/>
     <section name="profileService" type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
     <section name="authenticationService" type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
     <section name="roleService" type="System.Web.Configuration.ScriptingRoleServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
    </sectionGroup>
   </sectionGroup>
  </sectionGroup>

In the <assemblies> section add the following:

<add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

In the <pages> section add the following:

<CONTROLS><ADD assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.UI" tagPrefix="asp"></ADD><ADD assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.UI.WebControls" tagPrefix="asp"></ADD></CONTROLS>

In the <httpHandlers> section add the following:

   <remove verb="*" path="*.asmx"/>
   <add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
   <add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
   <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false"/>

In the <httpModules> section add the following:

   <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

Immediately before the </configuration> element at the bottom of the web.config add the following:

 <system.webServer>
  <validation validateIntegratedModeConfiguration="false"/>
  <modules>
   <remove name="ScriptModule"/>
   <add name="ScriptModule" preCondition="managedHandler" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
  </modules>
  <handlers>
   <remove name="WebServiceHandlerFactory-Integrated"/>
   <remove name="ScriptHandlerFactory"/>
   <remove name="ScriptHandlerFactoryAppServices"/>
   <remove name="ScriptResource"/>
   <add name="ScriptHandlerFactory" verb="*" path="*.asmx" preCondition="integratedMode" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
   <add name="ScriptHandlerFactoryAppServices" verb="*" path="*_AppService.axd" preCondition="integratedMode" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
   <add name="ScriptResource" preCondition="integratedMode" verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
  </handlers>
 </system.webServer>
 <runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
   <dependentAssembly>
    <assemblyIdentity name="System.Web.Extensions" publicKeyToken="31bf3856ad364e35"/>
    <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
   </dependentAssembly>
   <dependentAssembly>
    <assemblyIdentity name="System.Web.Extensions.Design" publicKeyToken="31bf3856ad364e35"/>
    <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
   </dependentAssembly>
  </assemblyBinding>
 </runtime>

<system.serviceModel>
    <bindings>
      <webHttpBinding>
        <binding name="webHttpAuthenticated">
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Ntlm" />
          </security>
        </binding>
      </webHttpBinding>
    </bindings>
    <behaviors>
      <endpointBehaviors>
        <behavior name="MyWSSListServiceAspNetAjaxBehavior">
          <enableWebScript />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
    <services>
      <service name="MyWSSListService">
        <endpoint address="" behaviorConfiguration="MyWSSListServiceAspNetAjaxBehavior" binding="webHttpBinding" bindingConfiguration="webHttpAuthenticated" contract="MyWSSListService" />
      </service>
    </services>
  </system.serviceModel>

Note the <system.serviceModel> element. This is the configuration for the WCF service we are about to add.

The next step is to actually create the web part. Create a new SharePoint web part project named DynamicWCFPart (I'm using the Visual Studio Extensions for SharePoint 1.2) but you can use whatever you want :)

Once the webpart project is created change everything you see from WebPart1 to MyDynamicPart (you will need to go into WSP view and change this part too). 

Next add a new SharePoint Template. Name the template item MyWSSListService.svc.

Beneath the Templates directory in Solution Explorer create a new folder called LAYOUTS and beneath LAYOUTS create another folder called MyDynamicWCFPart. Drag the MyWSSListService.svc into the MyDynamicWCFPart folder.

The next step is to add another SharePoint Template to your project. This time name it MyDynamicPartCtrl.ascx. This will be the UI of the webpart.

Once the template is added expand the Templates folder and create a new folder beneath it called CONTROLTEMPLATES then create another folder called MyDynamicWCFPart. Drag the MyDynamicPartCtrl.ascx into the newly created MyDynamicWCFPartfolder.

Finally, your solution should look similar to this:

To get your site ready you will need to create a custom list in your SharePoint site called Clients. The custom list should contain the following fields: Title (the existing one), Address,  City, State and Zip. All of the fields will be of type "Single Line of Text". You should also add some test data into the list as this webpart sample only edits (no inserts).

Next, add the following references:

System.Web.Extensions (3.5.0.0), System.ServiceModel (3.0.0.0), System.ServiceModel.Web (3.5.0.0), System.Runtime.Serialization (3.0.0.0).

Next we'll actually create the WCF service. We'll add the code inline to the .svc file that way it is deployable along with our webpart.

<%@ ServiceHost Language="C#" Debug="true" Service="MyWSSListService" %>

using System;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using Microsoft.SharePoint;
using System.Collections.Generic;

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MyWSSListService
{
    [OperationContract]
    public void UpdateClient(String UniqueID, String PhoneNumber, String Address, String City, String State, String Zip)
    {
        SPWeb spw = SPContext.Current.Web;
        spw.AllowUnsafeUpdates = true;
        SPList spl = spw.Lists["Clients"];
        SPListItem spli = spl.Items[new Guid(UniqueID)];
        spli["Phone"] = PhoneNumber;
        spli["Address"] = Address;
        spli["City"] = City;
        spli["State"] = State;
        spli["Zip"] = Zip;
        spli.Update();
        spw.AllowUnsafeUpdates = false;
    }

    [OperationContract]
    public ClientInformation GetClient(String UniqueId)
    {
        SPWeb spw = SPContext.Current.Web;
        SPList spl = spw.Lists["Clients"];

        SPListItem spli = spl.Items[new Guid(UniqueId)];        
        ClientInformation client =  new ClientInformation();
        client.Name = spli["Title"].ToString();
        client.Address = spli["Address"].ToString();
        client.Phonenumber = spli["Phone"].ToString();
        client.City = spli["City"].ToString();
        client.State = spli["State"].ToString();
        client.Zip = spli["Zip"].ToString();
        client.UniqueId = spli["UniqueId"].ToString();

        return client;
    }

    [OperationContract]
    public List<ClientInformation> GetClients()
    {
        SPWeb spw = SPContext.Current.Web;
        SPList spl = spw.Lists["Clients"];
        List<ClientInformation> clients = new List<ClientInformation>();
       
        foreach (SPListItem spli in spl.Items)
        {
            ClientInformation cl = new ClientInformation();
            cl.Name = spli["Title"].ToString();
            cl.Address = spli["Address"].ToString();
            cl.City = spli["City"].ToString();
            cl.State = spli["State"].ToString();
            cl.Phonenumber = spli["Phone"].ToString();
            cl.Zip = spli["Zip"].ToString();
            cl.UniqueId = spli["UniqueId"].ToString();
            clients.Add(cl);

        }

        return clients;
    }
   
}

[DataContract]
public class ClientInformation
{
    private String _uniqueid = String.Empty;

    [DataMember]
    public String UniqueId
    {
        get { return _uniqueid; }
        set { _uniqueid = value; }
    }

    private String _name = String.Empty;

    [DataMember]
    public String Name
    {
        get { return _name; }
        set { _name = value; }
    }
    private String _address = String.Empty;

    [DataMember]
    public String Address
    {
        get { return _address; }
        set { _address = value; }
    }
    private String _city = String.Empty;

    [DataMember]
    public String City
    {
        get { return _city; }
        set { _city = value; }
    }
    private String _state = String.Empty;

    [DataMember]
    public String State
    {
        get { return _state; }
        set { _state = value; }
    }
    private String _zip = String.Empty;

    [DataMember]
    public String Zip
    {
        get { return _zip; }
        set { _zip = value; }
    }
    private String _phonenumber = String.Empty;

    [DataMember]
    public String Phonenumber
    {
        get { return _phonenumber; }
        set { _phonenumber = value; }
    }

}

Finally, you should build your project to make sure you have all of the references correct.

A few things to note about the WCF service. First, in order to get a reference to the SPContext object correctly the service's class needs to have the [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] attribute added. The second thing to note is the ClientInformation class. It is marked with the [DataContract] attribute along with its members marked with [DataMember] so WCF will know to serialize this to our our client.

Our next step is to add our user interface. Since there is quite a bit of JavaScript involved here I felt it would be easier to design the UI in an ASP.NET User Control (.ASCX).

Open the MyDynamicPartCtrl.ascx control in Visual Studio and paste in the following code:

<%@ Control Language="C#" ClassName="MyDynamicPartCtrl" %>


<asp:ScriptManager ID="scriptManager" runat="server">
    <Services>
        <asp:ServiceReference Path="_layouts/MyDynamicWCFPart/MyWSSListService.svc" />
    </Services>
</asp:ScriptManager>

<script language="javascript" type="text/javascript">
    // Start up - call the service to retrieve our clients from the list
    GetClients();
    function GetClients()
    {
        MyWSSListService.GetClients(OnClientsReceived);
    }
    // Populate our dropdown list with the results of our WCF call    
    function OnClientsReceived(Clients)
    {
        var selClients = document.getElementById('selClients');
        selClients.options.length=0;
        for(var cCount=0; cCount < Clients.length; cCount++)
        {
            var opt = document.createElement("option");
            opt.value = Clients[cCount].UniqueId;
            opt.text = Clients[cCount].Name;
            selClients.options.add(opt);
        }
    }
    // When the user selects a client
    function SelectClient()
    {
        var selClients = document.getElementById('selClients');
        var clientID = selClients.options[selClients.selectedIndex].value;
        MyWSSListService.GetClient(clientID, OnClientSelected);
    }
    // Retrieve the data and populate our form with the selected client
    function OnClientSelected(Client)
    {
        ShowEdit();
        var name = document.getElementById("name");
        name.innerHTML = Client.Name;
        var phone = document.getElementById("phone");
        phone.value = Client.Phonenumber;
        var address = document.getElementById("address");
        address.value = Client.Address;                       
        var city = document.getElementById("city");
        city.value = Client.City;                       
        var state = document.getElementById("state");
        state.value = Client.State;                                   
        var zip = document.getElementById("zip");
        zip.value = Client.Zip;                       
        var hidUniqueId = document.getElementById("hidUniqueId");
        hidUniqueId.value = Client.UniqueId;
    }
    // Code to update the client through the service with the modified data
    function UpdateClient()
    {
        var phone = document.getElementById("phone").value;
        var address = document.getElementById("address").value;
        var city = document.getElementById("city").value;
        var state = document.getElementById("state").value;
        var zip = document.getElementById("zip").value;
        var uniqueid = document.getElementById("hidUniqueId").value;
        MyWSSListService.UpdateClient(uniqueid, phone, address, city, state, zip);
        HideEdit();
    }
    // Helper functions to hide/show our edit form
    function ShowEdit()
    {
        var divEdit = document.getElementById("divEdit");
        divEdit.style.display = "block";
    }
    function HideEdit()
    {
        var divEdit = document.getElementById("divEdit");
        divEdit.style.display = "none";
    }

</script>
<div style="width: 275px">
    <select id="selClients" onchange="javascript:HideEdit()" style="font-size: small">
    </select>
    <input type="button" onclick="javascript:SelectClient()" value="Edit Client" />
   
    <br /><br />
    <div style="display: none" id="divEdit">
        <input type="hidden" id="hidUniqueId" />
        <table style="width: 250px">
            <tr>
                <td colspan="2" style="font-weight: bold">
                    Client Information
                    <hr />
                </td>
            </tr>
            <tr>
                <td>
                    Name
                </td>
                <td>
                    <span id="name"></span>
                </td>
            </tr>            
            <tr>
                <td>
                    Phone
                </td>
                <td>
                    <input type="text" id="phone" />
                </td>
            </tr>
            <tr>
                <td>
                    Address
                </td>
                <td>
                    <input type="text" id="address" />
                </td>
            </tr>
            <tr>
                <td>
                    City
                </td>
                <td>
                    <input type="text" id="city" />
                </td>
            </tr>     
            <tr>
                <td>
                    State
                </td>
                <td>
                    <input type="text" id="state" />
                </td>
            </tr> 
            <tr>
                <td>
                    Zip
                </td>
                <td>
                    <input type="text" id="zip" />
                </td>
            </tr>     
            <tr>
                <td>
                    <input type="button" value="Update Client" onclick="UpdateClient()" />
                </td>
            </tr>                                                                                                       
        </table>
    </div>
</div>

This is definitely not the fanciest UI possible :) What it does is essentially gives you a dropdown list of "clients" to edit. Once you select a client and click the "Edit Client" button the service method GetClient is called. OnClientSelected is the callback when our client is retrieved. From there I populate and show a small form to allow the user to update the list data via the UpdateClient() service method.

To display the user control within the webpart we'll need to dynamically load the control and insert it into the control tree. Open MyDynamicPart.cs and add the following code to your CreateChildControls() method.

UserControl uc = (UserControl)Page.LoadControl("~/_controltemplates/MyDynamicWCFPart/MyDynamicPartCtrl.ascx");
Controls.Add(uc);

The next step is actually a compatability workaround to ensure SharePoint and WCF work well together. For details on the compatability issue see Gille's blog entry here: http://blogs.msdn.com/gzunino/archive/2007/09/17/hosting-a-wcf-service-in-windows-sharepoint-services-v3-0.aspx.

Add a a new C# class to your project and name the file: MyWCFVirtualPathProvider.cs. Replace the code with the following:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Hosting;

public class MyWCFVirtualPathProvider : VirtualPathProvider
{

    public override string CombineVirtualPaths(string basePath, string relativePath)
    {
        return Previous.CombineVirtualPaths(basePath, relativePath);
    }

    public override System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType)
    {
        return Previous.CreateObjRef(requestedType);
    }

    public override bool DirectoryExists(string virtualDir)
    {
        return Previous.DirectoryExists(virtualDir);
    }

    public override bool FileExists(string virtualPath)
    {
        // Patches requests to WCF services: That is a virtual path ending with ".svc"     
        string patchedVirtualPath = virtualPath;
        if (virtualPath.StartsWith("~", StringComparison.Ordinal) &&
          virtualPath.EndsWith(".svc", StringComparison.InvariantCultureIgnoreCase))
        {
            patchedVirtualPath = virtualPath.Remove(0, 1);
        }
        return Previous.FileExists(patchedVirtualPath);
    }

    public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath,
               System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }

    public override string GetCacheKey(string virtualPath)
    {
        return Previous.GetCacheKey(virtualPath);
    }

    public override VirtualDirectory GetDirectory(string virtualDir)
    {
        return Previous.GetDirectory(virtualDir);
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        return Previous.GetFile(virtualPath);
    }

    public override string GetFileHash(string virtualPath, System.Collections.IEnumerable virtualPathDependencies)
    {
        return Previous.GetFileHash(virtualPath, virtualPathDependencies);
    }

    protected override void Initialize()
    {
        base.Initialize();
    }
}

Next we're going to create an HttpModule to register the virtual path provider we just created. Add another C# class to your project and name it MyWCFPatchupModule.cs. Replace the code with the following:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Hosting;

public class MyWCFPatchupModule : IHttpModule
{

    static bool virtualPathProviderInitialized = false;
    static object virtualPathProviderInitializedSyncLock = new object();

    public void Dispose()
    {
    }

    public void Init(HttpApplication context)
    {
        if (!virtualPathProviderInitialized)
        {
            lock (virtualPathProviderInitializedSyncLock)
            {
                if (!virtualPathProviderInitialized)
                {
                    MyWCFVirtualPathProvider vpathProvider = new MyWCFVirtualPathProvider();
                    HostingEnvironment.RegisterVirtualPathProvider(vpathProvider);

                    virtualPathProviderInitialized = true;
                }
            }
        }
    }
}


You will need to add one more entry to the web.config file of the SharePoint site.

<add name="MyWcfVirtualPathProvider" type="MyWCFPatchupModule, DynamicWCFPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=[YourPublicKeyTokenHere]" />

Note you can find your public key token by bringing up a Visual Studio command prompt and from your projects \Debug directory running the following command: sn -T DynamicWCFPart.dll

Deploy your project by right clicking on your project and selecting Deploy.

The next step will be to put your page into edit mode by clicking Site Actions -> Edit Page. Select a zone and click Add a Web Part. Under the miscellaneous category you will see your part MyDynamicPart Web Part. Select it and click Add.

If everything worked you should have something that looks like this.

This example retrieves the data from the list and updates the list with no postbacks. Of course it could use some UI flashiness, error handling, and insert functionality - but this should get you started!

 


 

How to implement IRuntimeFilter2 or how to target/filter web parts.

If you are looking at the various blog entries regarding IRuntimeFilter and are left wondering why it is considered a “deprecated” interface in SharePoint 2007 wonder no more! The functionality of IRuntimeFilter is not gone forever it has just been updated with newer functionality in IRuntimeFilter2. The primary functionality difference between the two interfaces is instead of implementing the filtering UI in a separate ASP.NET dialog you are now able to implement this directly in your very own web control.

This entry goes into detail on how to use this new interface. To save time I’ve borrowed the implementation of IRuntimeFilter created by Michael O’Donovan at http://blogs.msdn.com/modonovan/archive/2005/07/07/436394.aspx and updated it for IRuntimeFilter2.

The first major difference you’ll find between the two interfaces is with the InitializeStrings method.  The original implementation defined the title, tooltip, button text and dialog sizing properties for a custom dialog you build and specify with the BuilderURL attribute in the <RuntimeFilter element of the web.config.  (Note: The BuilderURL is no longer needed in implementations of IRuntimeFilter2)

IRuntimeFilter (Original)

public ArrayList InitializeStrings(CultureInfo cultureInfO)

{

 ArrayList strings = new ArrayList();

 strings.Add("Role Picker");

 strings.Add("Lets you pick which user roles can view this web part");

 strings.Add("Pick Role");

 strings.Add("dialogHeight:340px;dialogWidth:430px;help:no;status:no;resizable:no");

 return strings;

}

 

With IRuntimeFilter2 you only need to specify the title for your new web control.

IRuntimeFilter2

public ArrayList InitializeStrings(CultureInfo cultureInfO)

{

ArrayList strings = new ArrayList();

strings.Add("Choose Groups To Filter");

return strings;

}

  

The only new method in IRuntimeFilter2 is GetToolPaneControl(). This method is called by SharePoint to get an instance of the WebControl you are specifying for the user interface of the filter.

 

#region IRuntimeFilter2 Members

public IToolPaneControl GetToolPaneControl()

{

DevConWebControl.RFWebControl rfwc = new DevConWebControl.RFWebControl();

rfwc.ID = "ID_RDWebControl";

return rfwc;

}

#endregion

 

The implementation of RFWebControl is relatively straightforward. To keep things simple I’m only filtering on the current sites available groups but you can filter on anything.

 

    public class RFWebControl : WebControl, IToolPaneControl, INamingContainer, IPostBackDataHandler

    {

        // The listbox that holds the groups available for filtering

        ListBox lb;

       

        // Custom list entry so I know when the user wants to clear the filter

        const string CLEARVALUE = "Clear Filter";

 

        protected override void CreateChildControls()

        {

            base.CreateChildControls();

           

            lb = new ListBox();

            lb.ID = "ID_lbSelectedGroups";

            lb.SelectionMode = ListSelectionMode.Multiple;

 

            lb.Items.Add(CLEARVALUE);

            // Get the available groups available to filter on

            foreach (SPGroup spg in SPContext.Current.Web.Groups)

            {

                lb.Items.Add(spg.Name);

            }

            this.Controls.Add(lb);

 

 

            // Initialize listbox if there were groups previously selected

            if (Text.Length > 0)

            {

                String[] groups = Text.Split(new char[] { ',' });

                foreach (String g in groups)

                {

                    foreach (ListItem li in lb.Items)

                    {

                        if (g.CompareTo(li.Text) == 0)

                            li.Selected = true;

                    }

                }

            }

        }

 

        protected override void OnPreRender(EventArgs e)

        {

            base.OnPreRender(e);

            // Register for postback so we can extract the selected

            // groups out in time to hand off to SharePoint

            this.Page.RegisterRequiresPostBack(this);

        }

 

        #region IToolPaneControl Members

        // This is our implementation of IToolPaneControl

        // SharePoint uses this value to persist the selected filter

        String _filterText = "";

        public string Text

        {

            get

            {

                return _filterText;

            }

            set

            {

                _filterText = value;

            }

        }

        #endregion

 

        #region IPostBackDataHandler Members

 

        bool IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)

        {

            String controlID = "ID_lbSelectedGroups";

            String fullID = "";

 

            // Loop through the controls until we find our listbox

            foreach (String k in postCollection.Keys)

            {

                if (k.Contains(controlID))

                    fullID = k;

            }

 

            if (postCollection[fullID] != null)

            {

                // This will contain the groups selected by the user

                // So we need to set the Text property with the filter

                Text = postCollection[fullID];

 

                // If the user selected clear empty the filter

                if (Text.CompareTo(CLEARVALUE) == 0)

                    Text = "";

            }

            return true;

        }

 

        void IPostBackDataHandler.RaisePostDataChangedEvent()

        {

        }

        #endregion

    }

 

The other major difference in the IRuntimeFilter2 implementation is since I’m filtering on groups stored in comma separated values my utility function CanUserSeePart() is different.

  

private bool CanUserSeePart(string groups)

{

        SPUser spu = SPContext.Current.Web.CurrentUser;

 

        String [] tmpGroups = groups.Split(new char[] { ',' });

 

        foreach (String g in tmpGroups)

        {

            foreach (SPGroup spg in spu.Groups)

                if (g.CompareTo(spg.Name) == 0)

                    return true;

        }

        return false;

}

  

To try this sample out on your own download the project from the post attachment. Once the solution is built you will need to register both DevConRuntimeFilter.dll and DevConWebControl in the global assembly cache using gacutil –i DevConRuntimeFilter.dll and gacutil -i DevConWebControl.

 

You will need to register your filter in the web.config of your SharePoint site. Keep in mind there can only be one RuntimeFilter per virtual server so if you are testing on a MOSS install you will need to comment out the original AudienceManager filter before adding this one.

 

The safest place to register your filter is immediately before the ending SharePoint element </SharePoint>.

 

 <RuntimeFilter Assembly="DevConRunTimeFilter, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b9e17e23dc48e734" Class="DevConRunTimeFilter.RuntimeFilter" />

 

Run IISReset and you should now be able to see the sample when you bring up the “Modify Shared Web Part” menu.

Page view tracker