Monday, October 20, 2008

SharePoint: Creating Timer Jobs

Its been quiet some time since my last post. Been hectic at work lately, finally some relief. Today, I want to discuss how to create timer jobs in SharePoint.  A timer job is a schedule of when to run a service by using the SharePoint Timer service.
SharePoint Service Timer service is a service that runs other services according to schedules specified in timer jobs. Many features in MOSS/Windows SharePoint Services 3.0 rely on timer jobs to run services according to a schedule.

Lets break down entire process into simpler steps. 
Step 1. Timer Job Development
Step 2. Timer Job Deployement Methodology
Step 3. Enable Timer Job

Step 1. Timer Job Development
The first thing you need to do is create a class that inherits from the Microsoft.SharePoint.Administration.SPJobDefinition class.

Let's look into WSS object model.  [Click on the image to enlarge]


To implement this class, one needs to create a few constructors and override the Execute() method. 

[code]
namespace sandeep {
 public class MyJob: SPJobDefinition{

public MyJob(): base(){
}

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

public MyJob(string jobName, SPWebApplication webApplication)
: base (jobName, webApplication, null, SPJobLockType.ContentDatabase) {
this.Title = "My SharePoint Task Job";
}

public override void Execute () {
// your business logic goes here
}
}
}
[/code]

Step 2. Timer Job Deployement Methodology
MSDN suggests, that the best way to deploy a timer job is to wrap it as a feature. On feature activated and deactivating events we can write code to add/delete timer job in context of current web/site.

Take a look at the code below:-
[code]
namespace sandeep {
class FeatureToEnableJob: SPFeatureReceiver {
const string TASK_LOGGER_JOB_NAME = "My Custom Job Name";

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

public override void FeatureActivated (SPFeatureReceiverProperties properties) {
SPSite site = properties.Feature.Parent as SPSite;

// make sure the job isn't already registered
foreach (SPJobDefinition job in site.WebApplication.JobDefinitions) {
if (job.Name == TASK_LOGGER_JOB_NAME)
job.Delete();
}

// install the job
MyJob job = new MyJob(TASK_LOGGER_JOB_NAME, site.WebApplication);

SPMinuteSchedule schedule = new SPMinuteSchedule();
schedule.BeginSecond = 0;
schedule.EndSecond = 120;
schedule.Interval = 5;
job.Schedule = schedule;

job.Update();
}

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

// delete the job
foreach (SPJobDefinition job in site.WebApplication.JobDefinitions) {
if (job.Name == TASK_LOGGER_JOB_NAME)
job.Delete();
}
}
}
}
[/code]

Step 3. Enable Timer Job
Once we have timer job wrapped as feature, either one can go for manual feature deployement process or go the WSP way (better approach)

Manual Way:
  • Deploy the strongly named assembly to the GAC.
  • Reset IIS (required for SharePoint to "see" the new timer job in the GAC).
  • Specify the receiver class and assembly in feature.xml that contains the event receivers.
  • Install the feature using stsadm.
  • Activate the feature using stsadm or settings.aspx.
WSP Way:
Please refer to my past post for this. 

cheers,

Monday, October 13, 2008

SharePoint: Creating a Web Part with a Custom Tool Part

Today, I want to talk on, how to add multiple toolparts in a webpart property window. In one of my earlier post, I did mention a quick and dirty way as how one can add simple textbox in webpart property window without needing any toolparts for that matter. But when it comes to implementing reusable/pluggable collection of controls, using toolparts is best possible option. 
Now, the question is, how can one easily implement toolparts in custom or default SharePoint Webparts. I think, the best way to approach this problem is to dig into SharePoint object model. Please refer the image below:

WebPart class exposes an overridable method "GetToolParts" which returns an array of references to the new ToolPart objects that will be displayed in the property pane of the Web Part. The ToolPart objects are rendered by the tool pane in the order listed in the array. 
Following is a sample code for creating a toolpart:
[code]
  public class SampleToolPart : Microsoft.SharePoint.WebPartPages.ToolPart
    {
        // First, override the CreateChildControls method. This is where we create the controls. 
        protected override void CreateChildControls()
        {
            // create a panel that will hold all of our controls 
            Panel toolPartPanel = new Panel();            
           
            // create the actual control 
            DropDownList sampleDropDown1 = new DropDownList();
            sampleDropDown1.ID = "sampleDropDown1";
            sampleDropDown1.Items.Add("Item 1");
            sampleDropDown1.Items.Add("Item 2");
            sampleDropDown1.Items.Add("Item 3");

            toolPartPanel.Controls.Add(sampleDropDown1);            

            // finally add the panel to the controls collection of the tool part 
            Controls.Add(toolPartPanel);

            base.CreateChildControls();
        }

        // Next, override the ApplyChanges method. 
        // This method is where we will persist the values that the user selects. 
        public override void ApplyChanges()
        {
            // get the parent webpart 
            SampleWebPart parentWebPart = (SampleWebPart)this.ParentToolPane.SelectedWebPart;

            // loop thru this control's controls until we find the ones that we need to persist. 
            RetrievePropertyValues(this.Controls, parentWebPart);
        }

        // Recursive function that tries to locate the values set in the toolpart 
        private void RetrievePropertyValues(ControlCollection controls, SampleWebPart parentWebPart)
        {
            foreach (Control ctl in controls)
            {
                RetrievePropertyValue(ctl, parentWebPart);

                if (ctl.HasControls())
                {
                    RetrievePropertyValues(ctl.Controls, parentWebPart);
                }
            }
        }

        // Method for retrieving the values set by the user. 
        private void RetrievePropertyValue(Control ctl, SampleWebPart parentWebPart)
        {
            if (ctl is DropDownList)
            {
                if (ctl.ID.Equals("sampleDropDown1"))
                {
                    DropDownList drp = (DropDownList)ctl;
                    if (drp.SelectedItem.Value != "")
                    {
                        parentWebPart.myProperty = drp.SelectedItem.Value;
                    }
                }
            }
        }
    }
[/code]
All this toolpart would do is:
  • add dropdown box in webpart properties window
  • on applying changes, it assigns selected value from dropdown box to public property of base webpart class
Lets see out to add this custom toolpart in webpart's toolpart collection. To do this, we override the GetToolParts() method and place our new tool part in the ToolPart array. Take a look at the code below:
[code]
      public class SampleWebPart : Microsoft.SharePoint.WebPartPages.WebPart
    {
        private string _property1 = "Default Value";

        public SampleWebPart()
        {
            this.ExportMode = WebPartExportMode.All;
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            Label lbl = new Label();
            lbl.ID = "toolpart_webpart_lbl1";
            lbl.Text = _property1;
            Controls.Add(lbl);
        }

        public string myProperty
        {
            get
            {
                return _property1;
            }
            set
            {
                _property1 = value;
            }
        }        

        public override ToolPart[] GetToolParts()
        {
            // resize the tool part array 
            ToolPart[] toolparts = new ToolPart[3];
            // instantiate the standard SharePopint tool part 
            WebPartToolPart wptp = new WebPartToolPart();
            // instantiate the custom property toolpart if needed. 
            // this object is what renders our regular properties. 
            CustomPropertyToolPart custom = new CustomPropertyToolPart();
            // instantiate and add our tool part to the array. 
            // tool parts will render in the order they are added to this array. 
            toolparts[0] = new SampleToolPart();
            toolparts[1] = custom;
            toolparts[2] = wptp;
            return toolparts;
        }
    }
[/code]

WebPart Added On Page:
[before updation]

[after updation]

Hope this helped.

cheers

Wednesday, October 1, 2008

SharePoint: All webparts appear as ErrorWebParts when using SPLimitedWebPartManager

Hi! recently I was given a task to update properties of multiple webparts(custom developed webparts created by extending existing SharePoint OTB webparts). Now the trick was to fetch those webparts across pages multiple pages and update their properties and UI formatting instructions based on a configuration file which is provided at run time on feature activation.

Seems a simple enough task, where one needs to retreive page-url, fetch SPLimitedWebPart manager based on page-url. Use SPLimitedWebPart Manager to iterate through collection of webparts which exist of specified page URL and update properties based on configuration input.

Like any other developer, I started of by creating a console application and dumping code in it like a no mad. Half-way down the line when I began to query webparts collection using SPLimitedWebPart Manger, I see all webparts returned are of type "ErrorWebParts".

[code]
using (SPSite mysite = new SPSite("http://server:27158/"))
{
  using (SPWeb myweb = mysite.RootWeb)
  {
  myweb.AllowUnsafeUpdates = true;

using (SPLimitedWebPartManager manager = myweb.GetLimitedWebPartManager(
"/Pages/Home.aspx", System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared))
{
IEnumerator myWebPArtsEnum = manager.WebParts.GetEnumerator();
String xmlstring = "";
while (myWebPArtsEnum.MoveNext())
{
xmlstring += "
if (myWebPArtsEnum.Current.ToString() =="IST.MOSS.Webparts.ISTContentByQueryWebPart")
{  
xmlstring += " Title='" + ((System.Web.UI.WebControls.WebParts.WebPart)myWebPArtsEnum.Current).Title + "'";
xmlstring += " ID='" + ((System.Web.UI.WebControls.WebParts.WebPart)myWebPArtsEnum.Current).ID + "'";
xmlstring += " DisplayTitle='" + ((System.Web.UI.WebControls.WebParts.WebPart)myWebPArtsEnum.Current).DisplayTitle + "'";
((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current).ItemLimit = 20;
//((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current).CommonViewFields = "Title,Text;URL,URL;Forward_x0020_to_x0020_URL,Link;Show_x0020_for_x0020_Locations,Text;";
//PublishingWeb pw = PublishingWeb.GetPublishingWeb(myweb);
//((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current).Update(pw);
manager.SaveChanges((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current);
myweb.Update();
}
xmlstring += " />";
}
xmlstring += "";
}  
myweb.AllowUnsafeUpdates = false;
}
}
[/code]

This code loops through all webparts on all pages, checks to see if the webpart
is a ContentByQueryWebpart and updates some properties.

As soon as you use webpartmanager to fetch webpart, it will return your queried

webpart as Microsoft.SharePoint.WebPartPages.ErrorWebPart.

Why? Because of the following code is called by the specific properties of the ContentByQueryWebpart

(actually its parent, the CmsDataFormWebpart):

internal static string MakeServerRelativeUrl(string url)
{
    return concatenateUrls(SPContext.GetContext(HttpContext.Current).Site.ServerRelativeUrl, url);
}
The webpart will always call the SPContext, but from a console
application there is no web-context. Therefore when initiating the
ContentByQueryWebpart, it will always thow an exception like:

"An error occured while setting the value of this property:
Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart:MainXslLink -
Exception has been thrown by the target of an invocation."

A workaround for this would be to provide it with a context. Take a look at the
modifications done to rectify the problem -

[code]
using (SPSite mysite = new SPSite("http://server:27158/"))
{
using (SPWeb myweb = mysite.RootWeb)
{
myweb.AllowUnsafeUpdates = true;

bool isContextNull = false;
if (HttpContext.Current == null)
{
isContextNull = true;
HttpRequest request = new HttpRequest("", myweb.Url, "");
HttpContext.Current = new HttpContext(request, new HttpResponse(new StringWriter()));
HttpContext.Current.Items["HttpHandlerSPWeb"] = myweb;
}

using (SPLimitedWebPartManager manager = myweb.GetLimitedWebPartManager(
"/Pages/Home.aspx", System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared))
{
IEnumerator myWebPArtsEnum = manager.WebParts.GetEnumerator();
String xmlstring = "";
while (myWebPArtsEnum.MoveNext())
{
xmlstring += "
if (myWebPArtsEnum.Current.ToString() =="IST.MOSS.Webparts.ISTContentByQueryWebPart")
{
xmlstring += " Title='" + ((System.Web.UI.WebControls.WebParts.WebPart)myWebPArtsEnum.Current).Title + "'";
xmlstring += " ID='" + ((System.Web.UI.WebControls.WebParts.WebPart)myWebPArtsEnum.Current).ID + "'";
xmlstring += " DisplayTitle='" + ((System.Web.UI.WebControls.WebParts.WebPart)myWebPArtsEnum.Current).DisplayTitle + "'";
((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current).ItemLimit = 20;
//((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current).CommonViewFields = "Title,Text;URL,URL;Forward_x0020_to_x0020_URL,Link;Show_x0020_for_x0020_Locations,Text;";
//PublishingWeb pw = PublishingWeb.GetPublishingWeb(myweb);
//((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current).Update(pw);
manager.SaveChanges((Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart)myWebPArtsEnum.Current);
myweb.Update();
}
xmlstring += " />";
}
xmlstring += "";
}

if(isContextNull)
HttpContext.Current = null;
myweb.AllowUnsafeUpdates = false;
}
}
[/code]

Hope this helped.

-cheers