Tuesday 11 February 2014

Timer Job For Sandbox Solution

Timer Job For Sandbox Solution

Everybody knows that the sandbox solution on SharePoint 2010 has lots of limitations. One of these limitations is SharePoint 2010 Sandbox solution doesn't have a timer job. How many of us wished to have a timer job under a sandbox solution. Well, this post shows to implement a timer job under a sandbox solution.

Keywords: SharePoint, SharePoint 2010, Timer Job, Sandbox Solution.

Components;
1-      List A with one item a Workflow that starts off manually and when then an item changes.
2-      List B with one item that has an event receiver with item updating and item updated handlers.

Scenario
1-      On list A
a.        Start the workflow manually on the item on the list.
b.      The workflow starts by pausing 1 minute.
c.       After pausing, the work flow updates the item that is in list B.
2-      On list B
a.       On item updating;
                                                                           i.      When the event receiver starts, run the code of the timer job that you need to execute.
b.      On item updated;
                                                                           i.      Update the item on list A and start the workflow on this list programmatically.
3-      When the workflow start programmatically, we will go back to step 1.


Top
Extension
You can have lots of timer jobs that are managed by only List A and List B, you don’t need to have two lists for every timer job. This is by
1-      Have and ID field in list A and ID field in List B.
2-      Every item in List A will have a corresponding item in List B with the same ID. E.g. List A will have items (1, Add Task) & (2, Update Task). List B will have items (1, Add Task) and (2, Update Task).
3-      In List A
a.       Make the workflow updates the item in List B that has the same ID of current item.
4-      In List B
a.       On Item Updating;
                                                                           i.      Check the item ID and run the corresponding timer Job.
b.      On Item Updated;
                                                                           i.      Run the Workflow on List A on the item that has the same ID of list  B updated item.
The scenario will be explained by example in details in later.

Limitations
The timer job code should contain only Sandbox functions.
Very simple, very easy and very productive.
The next figure shows the timer job sandbox scenario.


Top
Source Code
We will create a sandbox VS solution that has;
1-      Content Type for WFs list “Workflows list, which is the list that will carry items; each item will be corresponding to a timer job”.
2-      List Definition for WFs.
3-      List Instance for WFs
4-      Content Type for ERs list” Event Receivers list, which is the list that will have event receivers for item updating and item updated event, each item in this list will be corresponding to a timer job”.
5-      List Definition ERs.
6-      List Instance for ERs.
7-      Workflow based on WFs Content Type and associated to WFs list.
8-      Event Receivers for ERs list

Steps
1-      Create a team site called Sandbox Timer Job
2-      Create Sandbox Solution Called Sandbox Timer Job and point to the Sandbox Timer Job site URL.
3-      Create Content Type  for WFs and name it WFsCT
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   <Field ID="{EE2C4296-B8F6-4E14-A3BF-D6479D619630}" Name="IsWorking" DisplayName="Is Working" Type="Boolean" Description="Determines if the WF or the ER is working " Group="_Sandbox TimerJob Columns" Required="FALSE" Sealed="FALSE" />
  <Field ID="{756A3594-231E-4F8D-BDED-B7CF62F72A6A}" Name="TimerJobID" DisplayName="Timer Job ID" Type="Text" Description="Timer Job ID" Group="_Sandbox TimerJob Columns" Required="FALSE" Sealed="FALSE" />
  <Field ID="{1AA858AE-56E6-4495-BC08-5048DDF58257}" Name="TimerJobName" DisplayName="Timer Job Name" Type="Text" Description="Timer Job Name" Group="_Sandbox TimerJob Columns" Required="FALSE" Sealed="FALSE" />
  <Field ID="{FBC6E407-8E46-4024-BA3D-7EFBDBB42370}" Name="LastTimeRan" DisplayName="Last Time Ran" Type="Text" Description="Last Time Ran" Group="_Sandbox TimerJob Columns" Required="FALSE" Sealed="FALSE" />
  <!-- Parent ContentType: Item (0x01) -->
  <ContentType ID="0x01007aa0ccf9b3354ce69a95c198aaa555e6"
               Name="WFsCT"
               Group="_Sandbox TimerJob Content Types"
               Description="This content type is used forworkflows list"
               Inherits="TRUE"
               Version="0">
    <FieldRefs>
      <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" DisplayName="Status" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{EE2C4296-B8F6-4E14-A3BF-D6479D619630}" Name="IsWorking" DisplayName="Is Working" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{756A3594-231E-4F8D-BDED-B7CF62F72A6A}" Name="TimerJobID" DisplayName="Timer Job ID" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{1AA858AE-56E6-4495-BC08-5048DDF58257}" Name="TimerJobName" DisplayName="Timer Job Name" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{FBC6E407-8E46-4024-BA3D-7EFBDBB42370}" Name="LastTimeRan" DisplayName="Last Time Ran" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
    </FieldRefs>
  </ContentType>
</Elements>
4-      Add List Definition From Content Type based off WFsCT and name it WFsDef and create list instance for it.
5-      Modify the View to show all columns by modifying the <ViewFields>section as the following.
<ViewFields>
          <FieldRef Name="Title" DisplayName="Status"/>
          <FieldRef Name="IsWorking" DisplayName="Is Working"/>
          <FieldRef Name="TimerJobID" DisplayName="Timer Job ID"/>
          <FieldRef Name="TimerJobName" DisplayName="Timer Job Name"/>
          <FieldRef Name="Log" DisplayName="Log"/>
</ViewFields>

Top
6-      List Instance with data should look.
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <ListInstance Title="Workflows"
                OnQuickLaunch="TRUE"
                TemplateType="10001"
                Url="Lists/WFs"
                Description="List that the workflow will be triggered from">
<Data>
      <Rows>
        <Row>
          <Field  Name='Title'> Add Task </Field>
          <Field  Name='IsWorking'>False</Field>
          <Field  Name='TimerJobID'>1</Field>
          <Field  Name='TimerJobName'>Add Task</Field>
          <Field  Name='LastTimeRan'>-</Field>
        </Row>
        <Row>
          <Field  Name='Title'> Update First Task </Field>
          <Field  Name='IsWorking'>False</Field>
          <Field  Name='TimerJobID'>2</Field>
          <Field  Name='TimerJobName'>Update First Task</Field>
          <Field  Name='LastTimeRan'>-</Field>
        </Row>
      </Rows>
    </Data>
  </ListInstance>
</Elements>
7-      Create Content type for the list that will have Event Receivers, call it ERsCT. Don’t create new columns, we will use the created ones
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <!-- Parent ContentType: Item (0x01) -->
  <ContentType ID="0x01001f77ec68827f46719fec01cf60c37685"
               Name="ERsCT"
               Group="_Sandbox TimerJob Content Types"
               Description="This content type is used event receivers list"
               Inherits="TRUE"
               Version="0">
    <FieldRefs>
      <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" DisplayName="Status" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{EE2C4296-B8F6-4E14-A3BF-D6479D619630}" Name="IsWorking" DisplayName="Is Working" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{756A3594-231E-4F8D-BDED-B7CF62F72A6A}" Name="TimerJobID" DisplayName="Timer Job ID" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{1AA858AE-56E6-4495-BC08-5048DDF58257}" Name="TimerJobName" DisplayName="Timer Job Name" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
      <FieldRef ID="{FBC6E407-8E46-4024-BA3D-7EFBDBB42370}" Name="LastTimeRan" DisplayName="Last Time Ran" Required="FALSE"  Sealed="FALSE" ShowInNewForm="TRUE" ShowInDisplayForm="TRUE" ShowInEditForm="TRUE" />
    </FieldRefs>
  </ContentType>
</Elements>
8-      Create List Definition From Content Type based of ERsCT with list instance and call it ERsDef. Also change the view to show all columns as sown above.
9-      List Instance with data should look like this


<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <ListInstance Title="Event Receivers"
                OnQuickLaunch="TRUE"
                TemplateType="10002"
                Url="Lists/ERs"
                Description="List that the event receivers (timerjob codes) are triggered from">
<Data>
      <Rows>
        <Row>
          <Field  Name='Title'> Add Task </Field>
          <Field  Name='IsWorking'>False</Field>
          <Field  Name='TimerJobID'>1</Field>
          <Field  Name='TimerJobName'>Add Task</Field>
          <Field  Name='LastTimeRan'>-</Field>
        </Row>
        <Row>
          <Field  Name='Title'> Update First Task </Field>
          <Field  Name='IsWorking'>False</Field>
          <Field  Name='TimerJobID'>2</Field>
          <Field  Name='TimerJobName'>Update First Task</Field>
          <Field  Name='LastTimeRan'>-</Field>
        </Row>
      </Rows>
    </Data>
  </ListInstance>
</Elements>

Top
Using SharePoint designer, create a reusable workflow based off WFsCT and write the following actions then publish


The most important part here is updating the Event Receivers List.
1-       Create event Receiver for ERsDef and the event sources are; Item Updateting and Item Updated with the following code;
public override void ItemUpdating(SPItemEventProperties properties)
{
            base.ItemUpdating(properties);
            if (properties.ListTitle == "Event Receivers")
            {
                int timerJobID = (properties.AfterProperties["TimerJobID"] != null) ? int.Parse(properties.AfterProperties["TimerJobID"].ToString()) : 0;
                if (timerJobID > 0)
                {
                    switch (timerJobID)
                    {
                        case 1:
                            AddTask(properties.OpenWeb());
                            break;
                        case 2:
                            UpdateFirstTask(properties.OpenWeb());
                            break;
                        default:
                            break;
                    }//end switch
                }//end if (timerJobId > 0)
            }//end if (properties.ListTitle == "Event Receivers")
         
 }//end function
public override void ItemUpdated(SPItemEventProperties properties)
{
            base.ItemUpdated(properties);
            if (properties.ListTitle == "Event Receivers")
            {
                //get timerJob ID
                int timerJobID = (properties.AfterProperties["TimerJobID"] != null) ? int.Parse(properties.AfterProperties["TimerJobID"].ToString()) : 0;
                //get the item that should be updated in WF list
                SPWeb web = properties.OpenWeb();
                SPSite site = web.Site;
                SPList WFsList = web.Lists["Workflows"];
                SPQuery query = new SPQuery();
                query.Query = "<Where><Eq><FieldRef Name='TimerJobID'/><Value Type='Text'>" + timerJobID.ToString() + "</Value></Eq></Where>";
                SPListItem WFListItem = null;
                SPListItemCollection WFListItems = WFsList.GetItems(query);
                if (WFListItems.Count > 0)
                {
                    WFListItem = WFListItems[0];
                    //update list item in the WF list
                    web.AllowUnsafeUpdates = true;
                    WFListItem["LastTimeRan"] = DateTime.Now.ToString(); ;
                    EventFiringEnabled = false;
                    WFListItem.Update();
                    EventFiringEnabled = true;
                    web.AllowUnsafeUpdates = false;
                    //find and start the workflow that is associated to this list item
                    var assoc = WFsList.ContentTypes["WFsCT"].WorkflowAssociations.GetAssociationByName("TimerJobWF-Assoc"CultureInfo.InvariantCulture);
                    if (assoc != null)
                    {
                        var result = site.WorkflowManager.StartWorkflow(WFListItem, assoc, string.Empty);
                    }
                }//end  if (WFListItems.Count > 0)
            }//end if (properties.ListTitle == "Event Receivers")
           
        }//end function
  private void AddTask(SPWeb web)
        {
            SPSite site = web.Site;
            SPList TasksList = web.Lists["Tasks"];
            SPListItem TaskItem = TasksList.Items.Add();
            web.AllowUnsafeUpdates = true;
            TaskItem["Title"] = "New task from the timer Job";
            EventFiringEnabled = false;
            TaskItem.Update();
            EventFiringEnabled = true;
            web.AllowUnsafeUpdates = false;
        }
private void UpdateFirstTask(SPWeb web)
{
            SPSite site = web.Site;
            SPList TasksList = web.Lists["Tasks"];
            if (TasksList.Items.Count > 0)
            {
                SPListItem TaskItem = TasksList.Items[0];
                web.AllowUnsafeUpdates = true;
                TaskItem["Title"] = "Ran at " + DateTime.Now.ToString ();
                EventFiringEnabled = false;
                TaskItem.Update();
                EventFiringEnabled = true;
                web.AllowUnsafeUpdates = false;
            }
}

Top
   12-      Deploy the solution.
   13-      Very Important , go to the workflow again and re-write the action Update Event Receivers. Every time you deploy the sandbox solution, you will need to re-write this action again.
   14-      Associate the Workflows list to the workflow TimerJobWorkFlow that has been created in SharePoint Designer. Name the association “TimerJobWF-Assoc”. Make the workflow starts manually and on change as show below.

15-      In Workflows list, select first item, click workflows in ribbon, start the workflow, enjoy.

No comments:

Post a Comment