Delete a ListItem from SharePoint ListView WebPart [XSLT]

The objective here is to delete a single or multiple ListItems from a single SharePoint List using a ListView WebPart i.e., to delete a ListItem from client side. In order to achieve this, we will be making an ajax call to the SharePoint WebService, Lists.asmx. SharePoint provides a number of WebServices with various functionalities. One of them is Lists.asmx. This service has many apis related with SharePoint List. We’re gonna call UpdateListItems api with the Delete command to delete a single or multiple ListItems accordingly. The url of this service is kept relative as, ../_vti_bin/lists.asmx.Following is the schema of this api.

POST /_vti_bin/Lists.asmx HTTP/1.1
Host: win-hk4iec2ge2r
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://schemas.microsoft.com/sharepoint/soap/UpdateListItems"

<?xml version="1.0" encoding="utf-8"?>
 <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
 <soap:Body>
 <UpdateListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/">
 <listName>string</listName>
 <updates>string</updates>
 </UpdateListItems>
 </soap:Body>
</soap:Envelope>

The approach is going to be pretty simple. We’ll be editing our custom XSLT as a normal HTML file with an exception of encoding the HTML schema of the api as we can not use any html tag inside an xml declaration. First we’ll define our HTML structure in the XSLT. Here, we will generate an HTML checkBox against the Title of all the items displayed. The IDs of these checkBoxes are the SharePoint ID of the corresponding ListItem they represent. This will help us to get the IDs of the ListItem that User wants to delete.



<table style="width:100%; padding-left: 0px">

<tr>

<td colspan="2" style="text-align: left; width: 40%"> 
 <input id="chkSelectAll" type="checkbox" name="checkbox" onclick="OnChangeCheckbox(this)" value="SelectAll" />
 <label id="SelectAll">Select All</label>
 </td>


<td style="text-align: left;">
 <button type="button" onclick="DeleteItems()" class="buttonv">Delete</button>
 </td>

 </tr>

</table>

Next declare the checkbox where the code for, for-each rows are written. As mentioned, the checkBoxes will appear against the title for each item in the view.


<div>


 <input type="checkbox" name="checkbox" value="{@ID}" id="{@ID}" onclick="ClearParent(this)"/>
 <a class="list_link" title="{@ListId_x003a_Title}" href="viewList.aspx?ListId={@ListId_x003a_ID}">
 <xsl:value-of select="@ListId_x003a_Title" disable-output-escaping="yes" />
 </a>
 
 <span class="submittedby"></span>
 
 <span class="summary">
 <xsl:value-of select="@ListId_x003a_ShortSummary" disable-output-escaping="yes" />
 </span>
 

</div>

Now all the JavaScript required to complete this operation.

        
        //This function gets the ID of all the ListItems that the user has selected and mark them for Deletion.
        function GetItemsToBeRemoved(){
          var counter = 0;
          var checkedIds = new Array();
          var cbs = document.getElementsByTagName('input');
          for(var i=0; i < cbs.length; i++) {               if(cbs[i].type == 'checkbox') {                   if(cbs[i].value != "SelectAll"){                       if(cbs[i].checked){                         checkedIds[counter++] = cbs[i].id;                       }                   }               }           }           return checkedIds;         }         //This is the function which is called on the Delete Button Click event. Only "" without encoding.         function DeleteItems(){           var checkedIds = GetItemsToBeRemoved();           if(checkedIds.length > 0){
            GenerateRequest(checkedIds);
          }
          else{
            alert('Plz select atleast 1 item to delete.');
          }
        }
        
        //Create and send the SOAP request. Again HTML tags are encoded. Since we'll be generating a Batch request for deletion, we have to generate this request in a for-loop.
        function GenerateRequest(checkedIds){

            var soapEnv = "<?xml version='1.0' encoding='utf-8'?><soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'><soap:Body><UpdateListItems xmlns='http://schemas.microsoft.com/sharepoint/soap/'><listName>SpListName</listName><updates><Batch>";
            var methodEnv1 = "<Method ID='1' Cmd='Delete'><Field Name='ID'>";
            var methodEnv2 = "</Field></Method>";
            var soapEnv2 = "</Batch></updates></UpdateListItems></soap:Body></soap:Envelope>";

 
            //Foreach selected item, create a delete request. 
            for (var i = 0; i < checkedIds.length; i++){
                soapEnv+=methodEnv1 + checkedIds[i] + methodEnv2;
            };

            soapEnv+=soapEnv2;

            //Finally send a single batch request for multiple items. 
            $.ajax({
                    url: "../_vti_bin/lists.asmx",
                    beforeSend: function(xhr) { xhr.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/sharepoint/soap/UpdateListItems");
                },
                type: "POST",
                dataType: "xml",
                data: soapEnv,
                complete: CheckForErrors,
                contentType: "text/xml; charset=\"utf-8\""
            });
        }

        //This function checks if any error has been encountered or not. If no error is found, then it re-Loads the current page to refresh the View.
        function CheckForErrors(xData, status) {

            //alert("Update Status : " + status );
            //alert("Update Status : " + xData.responseText );

            if(status == "success"){
                location.reload(true);
            }

        }

        //helper functions
        
        //Disable the delete button if there are no items to be displayed i.e., the SharePoint List is empty. Call this function on document.ready.
        function ValidateNoRecords(){
            var isDataFound = false;
            var cbs = document.getElementsByTagName('input');
            for(var i=0; i < cbs.length; i++) {
                if(cbs[i].type == 'checkbox') {
                    if(cbs[i].value != "SelectAll"){
                        isDataFound = true;
                        break;
                    }
                }
            }

            if(!isDataFound){
                var id = "#chkSelectAll";
                $(id).closest('tr').hide()
            }

        }

        //Checks whether to display the Delete button or not.
        $(document).ready(function(){
            ValidateNoRecords();
        })

***

SharePoint XSLT FormatDate Issue With Different DateTime Formats

Recently, I came across a weird issue. Our SharePoint site was running using the Locale, US [English]. We’re supposed to change it to UK i.e., to change the site’s DateTime format from mm/dd/yy to dd/mm/yy. One can easily do that in the Regional Settings option of their site. However, after the change, we noticed a discrepancy in the DateTime displayed in all our custom XSLTs.Say, for a Date, Friday, January 02, 2015, the value displayed after the change was Sunday, February 01, 2015! Clearly, it was the issue with the date format. Once the value 01/02/2015 [mm/dd/yyyy] got changed to 02/01/2015 [dd/mm/yyyy], something went wrong in our custom view WebPart. However, a Date value, where the date was more than 12, say, 20/12/2014 [dd/mm/yyyy] was being correctly displayed as, Friday, December 19, 2014 which, again, was very surprising. The current settings of the custom WebPart was:

<span class="submittedby">
    <xsl:text>Author:</xsl:text>
    <xsl:value-of select="@AuthorBy" disable-output-escaping="yes"/>  
    <xsl:text> on </xsl:text>
     <xsl:value-of select="ddwrt:FormatDate(string(@SomeDateField), 2057, 3 )"/>
</span>

where, &#xA0 (with semi-colon), denotes a single space character.

I was surprised. The LCID 2057[UK] was already being passed to the function, FormatDate, even when the site’s locale was set to US. Then why the webPart would behave correctly for the locale US and not for UK? It took me a while to figure out that the LCID [2057] that was being used. only affects the output from the method, FormatDate. It however, expects the DateTime in a given format only as input, which is, mm/dd/yyyy. So when the site locale was US, everything was working fine as the date returned by SharePoint was already in this format but, when I changed it to UK things got different.

One important thing to not here is, that the FormatDate function tolerates the obvious error like 20/12/2014 [dd/mm/yyyy] which automatically became, Friday, December 19, 2014, which indeed, is quite frustrating. Anyway, since we knew that in our case, the site locale would always remain UK, we applied a hard-code fix to this problem. Since, the method expects DateTime of type string, so instead of directly passing the Date field to it, we decided to construct the DateTime from dd/mm/yy[UK returned by SharePoint] to mm/dd/yyyy[US which the method accepts as the only input format] using the string-split approach. Following is the modified code snippet.

<span class="submittedby">
    <xsl:text>Author:</xsl:text><xsl:value-of select="@AuthorBy" disable-output-escaping="yes"/> <xsl:text> on </xsl:text>

    <!--original date in dd/mm/yyyy without time-->
    <xsl:variable name="dateFirst" select="substring-before(@SomeDateField,' ')"></xsl:variable>

    <!--date-->
    <xsl:variable name="date" select="substring-before($dateFirst,'/')"></xsl:variable>

    <!--month & Year part-->
    <xsl:variable name="dateRemaining" select="substring-after($dateFirst,'/')"></xsl:variable>

    <!--month-->
    <xsl:variable name="month" select="substring-before($dateRemaining,'/')"></xsl:variable>

    <!--year-->
    <xsl:variable name="year" select="substring-after($dateRemaining,'/')"></xsl:variable>


    <!--construct date as mm/dd/yyyy-->
    <xsl:variable name="pubDate" select="concat($month, '/', $date, '/', $year)"></xsl:variable>

    <!--use the date from the variable @pubDate-->
     <xsl:value-of select="ddwrt:FormatDate($pubDate, 2057, 3 )"/>

</span>

After this fix, everything apparently, returned back to normal, however we know that if tomorrow we try to use the same XSLT to a different site whose locale is not set to UK then, the same issue is bound to re-generate and we have to rework on our split logic.

Prevent SharePoint solutions from getting Deployed Globally

We must have noticed that for certain solution packages (.wsp), SharePoint only provides the option to Deploy it globally. This is a big drawback for applications that were meant to target certain Web Applications only.Due to the fact that the wsp is deployed globally, an IIS Pool recycle will be performed bringing down all the WebApplications, even though the current solution package (.wsp) was targeting only some of them (or even one of them).

To avoid this, simply follow the following steps:

=> In Visual Studio load your project.
=> Open Solution Explorer.
=> Double Click on the Package.package.
=> Once it’s opened, click on the Advanced option of this package
=> The Additional Assemblies screen will show up. Then, click the option, “Add Existing Assembly“. [Note: This option is valid only if your solution package does not depend on multiple assemblies and you can just add the current project’s dll. Otherwise, click on “Add Assembly from Project Output“.]

=> If you have selected the first option, “Add Existing Assembly” then, select the source path, of your current dll.
=> If you have selected the option, “Add Assembly from Project Output” then you have to select the source project instead of just a dll.
=> The location field will be automatically updated.
=> Leave the deployment target at GlobalAssemblyCache.
=> Copy the name of the dll from the Location field without “.dll” part.
=> Under SafeControls, Click to add a new item.
=> Double-Click the Namespace field and paste the item you just copied. Do the same for Assembly Name field.

=> Click OK to close the window.
=> You should now be able to see the following screen.

=> Select your project in Solution Explorer, press F4 and set “Include Assembly In Package” as False.

Finally, deploy your solution and check the same in Central Administration. You can now see that your wsp has been deployed to a given Web Application only.

Create a SharePoint Timer Job

Create a Blank SharePoint project in Visual Studio.Enter your site url and validate.

Add a new class say, TestTimer.

All timers must inherit from the class, Microsoft.SharePoint.Administration.SPJobDefinition. Following is the code sample for our TestTimer class.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.Administration;

namespace SharePointTimerSample
{
    class TestTimer : SPJobDefinition
    {
        public TestTimer()
            : base()
        {
        }

        public TestTimer(string jobName, SPService service, SPServer server, SPJobLockType targetType)
            : base(jobName, service, server, targetType)
        {
        }

        public TestTimer(string jobName, SPWebApplication webApplication)
            : base(jobName, webApplication, null, SPJobLockType.Job)
        {
            this.Title = "My Test Timer Job";
        }

        protected override bool HasAdditionalUpdateAccess()
        {
            return true;
        }

        public override void Execute(Guid contentDbId)
        {
            //Write your main code here
        }
    }
}

The main functionality of our Timer job has to be laid down in the “Execute” method. It is this method, which will get called periodically by the timer after the completion of its given time interval.

Now, add a new Feature to your project.

Set the scope of the timer as per your requirement. Here, we’re going to set its scope to the site level. You can also set the name of your timer job here. Plz note that this is the name that will be displayed under the SiteCollection features option and is not the name of your wsp. The wsp’s name is going to be the name of your project.

Add a EventReceiver to our newly created Feature.

It is at the EventReceiver, we define the activities that needs to be performed while activating or deactivating the timer job. Even the time interval is also defined here.


using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;

namespace SharePointTimerSample.Features.Feature1
{
    /// 
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// 
    /// 
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// 

    [Guid("e61e62e9-5b30-4c8b-ad03-402056f835e3")]
    public class Feature1EventReceiver : SPFeatureReceiver
    {
        // Uncomment the method below to handle the event raised after a feature has been activated.

        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                SPSite site = properties.Feature.Parent as SPSite;
                int userid = site.RootWeb.CurrentUser.ID;

                // make sure the job isn't already registered  
                site.WebApplication.JobDefinitions.Where(t => t.Name.Equals("List_JOB_NAME")).ToList().ForEach(j => j.Delete());

                // install the job  
                TestTimer listLoggerJob = new TestTimer("List_JOB_NAME", site.WebApplication);

                //SPDailySchedule schedule = new SPDailySchedule();
                //schedule.BeginHour = 17;
                //schedule.BeginMinute = 0;
                //schedule.BeginSecond = 0;
                //schedule.EndSecond = 59;
                //schedule.EndMinute = 59;
                //schedule.EndHour = 0;
 
                SPMinuteSchedule schedule = new SPMinuteSchedule();
                schedule.BeginSecond = 0;
                schedule.EndSecond = 59;
                schedule.Interval = 5;
                
                listLoggerJob.Schedule = schedule;
                listLoggerJob.Update();
            });
        }


        // Uncomment the method below to handle the event raised before a feature is deactivated.

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                SPSite site = properties.Feature.Parent as SPSite;

                // delete the job  
                site.WebApplication.JobDefinitions.Where(t => t.Name.Equals("List_JOB_NAME")).ToList().ForEach(j => j.Delete());
            });
        }


        // Uncomment the method below to handle the event raised after a feature has been installed.

        //public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        //{
        //}


        // Uncomment the method below to handle the event raised before a feature is uninstalled.

        //public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        //{
        //}

        // Uncomment the method below to handle the event raised when a feature is upgrading.

        //public override void FeatureUpgrading(SPFeatureReceiverProperties properties, string upgradeActionName, System.Collections.Generic.IDictionary parameters)
        //{
        //}
    }
}

Finally, select your Feature in the Solution Explorer, and change, “Activate On Default” to False and “Always Force Install” to True.

That’s it. Next, deploy your solution and activate the feature from the “Site Collection features” option of your site. [As we set the scope to Site level.]