Saturday, July 7, 2012

Use jQuery.ajax() without jQuery

Some time ago on on of the projects we needed to perform ajax calls, but can’t use jQuery (restriction came from customers). It was a bit tricky because we needed to use it with jsonp protocol and I didn’t want to implement all those things which are implemented in jQuery from scratch (including dynamic callback function which is passed in query string and executed when response is received).

If we can’t use jQuery.ajax(), we can try to copy only this part from jQuery and use it as standalone library (pure javascript without overriding ‘$’ symbol). It was actually not very hard. Just requires attention and perseverance. You need to copy jQuery.ajax() itself and all dependencies, including all methods and global variables which are used in them. It is defined as single object:

   1: var jQueryAjax = {
   2:  
   3:     noop: function() { },
   4:  
   5:     error: function(msg) {
   6:         throw msg;
   7:     },
   8:  
   9:     ...
  10: };

You can use it as general jQuery:

   1: jQueryAjax.ajax({
   2:     url: '/File/GetType',
   3:     datatype: 'json',
   4:     data: { filePath: val },
   5:     async: true,
   6:     success: function (res) {
   7:         $("#Type").val(res);
   8:     }
   9: });

I called file jQueryAjax-1.4.2.js (jQuery 1.4.2 was used) and uploaded it here. Decided to share it because it may be useful if you will need to do similar thing. It worked in our case (ajax calls with jsonp data type), but I’m not sure that it will work in all possible scenarios. Although I tried to be accurate when copy jQuery dependencies, it needs more testing. Anyway, hope that it will help someone.

Friday, July 6, 2012

Several ways to implement custom ULS logging service in Sharepoint

In Sharepoint 2010 you may implement custom logging service – inheritor of SPDiagnosticsServiceBase which will allow you to specify user friendly product name in ULS logs and add another custom behaviors. You may also use standard SPDiagnosticsService, but it will show you product name = Unknown which is not very useful. If you will search for examples, 2 popular approaches are the following:

1. singleton with static class property which creates instance of the logging service using private constructor. Example is shown in this post of Waldek Mastykarz:

   1: public class LoggingService : SPDiagnosticsServiceBase
   2: {
   3:     private LoggingService()
   4:         : base("My Logging Service", SPFarm.Local)
   5:     {
   6:     }
   7:  
   8:     private static LoggingService current;
   9:     public static LoggingService Current
  10:     {
  11:         get
  12:         {
  13:             if (current == null)
  14:             {
  15:                 current = new LoggingService();
  16:             }
  17:             return current;
  18:         }
  19:     }
  20:  
  21:     ...
  22: }

2. also uses static class property, but retrieves logging service instance from farm configuration database as shown in this example in msdn:

   1: public class LoggingService : SPDiagnosticsServiceBase
   2: {
   3:     public LoggingService()
   4:     { 
   5:     }
   6:  
   7:     public LoggingService(string name, SPFarm farm)
   8:         :base(name, farm)
   9:     {
  10:     }
  11:  
  12:     public static LoggingService Local
  13:     {
  14:         get
  15:         {
  16:             return GetLocal<LoggingService>();
  17:         }
  18:     }
  19: }

Both approaches work. Depending on your needs and priorities you may select 1st or 2nd way.

SPDiagnosticsService inherits SPPersistedObject so it is stored in serialized form in configuration database and Sharepoint deserializes it when we call GetLocal<T>() method using reflection. In order to do this it requires from logging service class 2 public constructors: parameterless and with 2 parameters (string, SPFarm) – otherwise you will get Constructor not found exception in runtime. 1st method uses constructor for creating the object, so advantage is that it is faster. Why do we need 2nd way then?

2nd way is needed if you want to make your custom logging service configurable via Central administration > Monitoring > Configure diagnostic logging. I.e. if you want to specify different trace and event severities for different categories via UI as you can do for standard categories, you should use 2nd way.

With 1st way even if you will register logging service in farm config database (by calling service.Update() method), changes in CA won’t affect your application because you will get instance via constructor each time. This instance will contain logging categories returned from its ProvideAreas method with default trace and event severities. But in CA when you select product name or particular category under it you may change these severities and it won’t affect instance created by constructor. Actually it won’t immediately affect your application even if you will try to optimize 2nd approach and will make it like this:

   1: public class LoggingService : SPDiagnosticsServiceBase
   2: {
   3:     public LoggingService()
   4:     { 
   5:     }
   6:  
   7:     public LoggingService(string name, SPFarm farm)
   8:         :base(name, farm)
   9:     {
  10:     }
  11:  
  12:     private static LoggingService current;
  13:     public static LoggingService Local
  14:     {
  15:         get
  16:         {
  17:             if (current == null)
  18:             {
  19:                 current = GetLocal<LoggingService>();
  20:             }
  21:             return current;
  22:         }
  23:     }
  24: }

In this case changes from CA will be applied only after next app pool recycling when LoggingService.current will be re-initialized.

With 2nd approach you will always get last copy of LoggingService with changes from CA. Note that if you won’t register service in config database and will try to use it, then most probably when call LoggingService.Current you will get Access denied error. It happens because internally it will try to register itself by calling SPPersistedObject.Update method and will fail while called in context of not-CA web application. This is known problem. As workaround you may call it in context of CA web app (e.g. in application layouts page). But recommended approach is to register it in feature receiver:

   1: var service = LoggingService.Current;
   2: if (service != null)
   3: {
   4:     service.Update();
   5: }

By the same way you may delete it from config database on feature deactivating event:

   1: var service = LoggingService.Current;
   2: if (service != null)
   3: {
   4:     service.Delete();
   5: }

This is all what I wanted to tell in this article. Hope it will help you when you will make choice of how to implement logging service.

Sunday, June 24, 2012

Sharepoint bug: incorrect itemId in summary alerts with custom alert handler

Some time ago I faced with strange behavior. Custom alert template with custom alert handler was used for alerts for particular list. Immediate alerts worked successfully and custom alert handler was called properly. However summary daily alerts didn’t work.

Custom alert handler is inheritor of IAlertNotifyHandler interface which allows you to override default behavior of alerts processing. E.g. you may log all alerts with custom handler. Here are several examples which shows how to implement custom alert handler step by step: SharePoint – Customizing Alert emails using IAlertNotifyHandler, How To: Customizing alert emails using IAlertNotifyHandler. Shortly you need to override IAlertNotifyHandler.OnNotification method. It has single parameter of SPAlertHandlerParams type. In turn it has SPAlertEventData[] eventData property.

When custom alert handler processes immediate alert (which is caused e.g. by adding new item) eventData has single element and its itemId property contains integer identifier of the list item which caused alert. Using this identifier you may open list item in your handler and read its metadata, e.g. determine user who modified this item.

When summary alert is processed, eventData contains several items – separate item for each event. The problem is that if you will iterate through collection and get eventData[i].itemId you will get any except correct integer identifier (it can be 0, 186, 1932917208, etc., while real identifiers will be 10, 11, 12). For me such behavior looks like a bug.

This issue was mentioned also in this forum thread: SPAlertHandlerParams - not behaving correctly for daily alerts. And author Johnny Dogbert provided workaround:

   1: string url = ahp.eventData[i].itemFullUrl;
   2: int itemId = int.Parse(url.Substring(url.LastIndexOf('/') + 1).Replace("_.000", ""));

It works because in itemFullUrl for list items Sharepoint passes the following URL: testsite/Lists/Test list/10_.000. I.e. we just get id from URL. Note that for documents it won’t most probably work – you will need to open SPFile from url and get its id using SPFile.Item.ID property.

This problem is reproducible on Sharepoint 2007. I didn’t test it on Sharepoint 2010, if you will test it please share the results in comments.

How to trigger and test daily alerts in Sharepoint

In Sharepoint it is possible to create alerts with different frequency:

  • immediate – sent immediately when next time immediate alerts job will run
  • daily – sent daily also by immediate alerts job
  • weekly – sent weekly

If you create new daily alert and want to see whether it will work or not it is not very convenient to wait 24 day until Sharepoint will sent them next time. In this post I will show several ways to trigger summary alerts and send them when you need.

Method 1. When you add a new daily alert, new row is added to the SchedSubscriptions table into Sharepoint content database. This is the key element of this method. We are interesting in the following 2 columns in this table:

  1. NotifyTime
  2. NotifyTimeUNC (NotifyTime minus 3 hours)

In these columns Sharepoint stores time when next time daily alert for particular list will be sent. So first of all determine row which corresponds to your list:

   1: SELECT * FROM SchedSubscriptions

Table contains SiteUrl, WebUrl, ListUrl columns. Using them you will be able to find needed row. Copy Id (uniqueidentifier) and execute the following SQL query:

   1: declare @s datetime
   2: declare @u datetime
   3: set @s = CAST('2012-06-24 12:00:00.000' as datetime)
   4: set @u = CAST('2012-06-24 09:00:00.000' as datetime)
   5:  
   6: update dbo.SchedSubscriptions
   7: set NotifyTime = @s, NotifyTimeUTC = @u
   8: where Id = '...'

In this example in Id you should specify value which you copied from previous query’s result, @s corresponds to NotifyTime, @u to NotifyTimeUNC (NotifyTime minus 3 hours). Time should be in past (comparing with current datetime) – only in this case Sharepoint will send daily alerts.

After that wait some time. Exact time of waiting depends on the job-immediate-alerts property which can be determined by the following command:

   1: stsadm -o getproperty -pn job-immediate-alerts -url http://example.com

for testing you can set it to 1 minute:

   1: stsadm -o setproperty -pn job-immediate-alerts -url http://example.com -pv "every 1 minutes between 0 and 59"

but after testing it is better to revert it e.g. to 5 minutes:

   1: stsadm -o setproperty -pn job-immediate-alerts -url http://example.com -pv "every 5 minutes"

So after this time if you will check SchedSubscriptions table you will see that time which you updated is increased by 1 day: in out example it will be “2012-06-25 12:00:00.000” for NotifyTime and “2012-06-25 09:00:00.000” for NotifyTimeUNC. It means that Sharepoint processed daily alert and it was sent. If everything is Ok, you or alert’s recipient should get email alert with daily summary.

Method 2. I found it in the following forum thread: SPAlertHandlerParams - not behaving correctly for daily alerts, but didn’t test it by myself. May be it will be useful for you as well:

   1: SPSite site = new SPSite("http://example.com");
   2: SPWeb web = site.OpenWeb();
   3: SPAlert alert = web.Alerts[new Guid("...")];
   4: alert.AlertFrequency = SPAlertFrequency.Daily; 
   5: alert.AlertTime = DateTime.Now.AddMinutes(1);
   6: alert.Update();

For general alerts troubleshooting I recommend the following articles: The Truth About How Daily SharePoint Alerts Actually Work, Troubleshooting Alerts. They will economy some time for you. Possibility to trigger daily alerts is very important for troubleshooting. It helped me in my work, hope it will be helpful for you as well.

Friday, June 22, 2012

Fix “Invalid character” error from ScriptResource.axd

Some time ago on one of our projects customers faced with the following problem: 2 users couldn’t use functionality which was implemented with javascript. For all other users everything worked, but for these 2 users it didn’t work. We checked their browsers – they used IE8. Most of other users used IE9. The first thing we tried is to run site in IE9 with IE8 browser mode and document mode (you can change it from developer tools in IE). But it didn’t give result – we couldn’t reproduce problem locally. Then I asked to send us screenshots of the pages with broken functionality and noticed that there was javascript error icon in status bar. Customers send us description of the error:

   1: User Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0;
   2: SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729;
   3: Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E)
   4: Timestamp: Thu, 22 Jun 2012 09:00:00 UTC 
   5:  
   6: Message: Invalid character
   7: Line: 1
   8: Char: 1
   9: Code: 0
  10: URI: https://example.com/ScriptResource.axd?d=... 
  11: Message: 'Sys' is undefined
  12: Line: 148
  13: Char: 1
  14: Code: 0
  15: URI: https://example.com
  16:  
  17: Message: 'Type' is undefined
  18: Line: 2
  19: Char: 1
  20: Code: 0
  21: URI: https://example.com
  22:  
  23: Message: Object doesn't support this property or method
  24: Line: 2
  25: Char: 132
  26: Code: 0
  27: URI: https://example.com

(there were more similar errors, I copied only distinct). After brief investigation and googling I found that some time this error can be fixed by disabling script compression. It can be done by adding the following line into web.config:

   1: <system.web.extensions>
   2:   <scripting>
   3:     <scriptResourceHandler enableCompression="false" enableCaching="true" />
   4:     ...
   5:   </scripting>
   6: </system.web.extensions>

And it helped. After I added this line into web.configs on all WFEs error disappeared. Users with other browsers also still work without problems. Hope it will help you if you will encounter with similar problem.

Update 2012-06-27: another way to fix this problem is to add site to the Local Intranet zone in IE settings. After that functionality also worked without problems even after we removed scriptResourceHandler from web.config.

Saturday, June 16, 2012

Quick way to view event log from remote computer

In Sharepoint development we often have a deal with farm environments. Typical farm contains db server, app server and several WFEs. If something goes wrong one of the first thing which we make for troubleshooting is checking of Sharepoint logs and Windows event logs. Each WFE has own logs and we need to check all of them in order to be sure that don’t miss something. In order to speed up the process we may setup regular Windows share for folder with Sharepoint logs (14/Logs by default) and open it from single WFE to which we are connected by Remote desktop. But what about event log? Is there a way to quickly open it from remote computer without connecting to it via RDP? Yes it is and this way is quite simple. You need to use the following command:

eventvwr <computer name or IP address>

and it will open event log from remote computer in your RDP session. You may also connect from UI: open event log, right click top level node and select Connect to another computer:

image

Then you need to specify computer name or its IP address connect to it. More details can be found in the following technet article: Work with Event Logs on a Remote Computer.

Wednesday, June 13, 2012

Camlex.Net 3.2: add expressions to existing string queries

About half year ago we added non-trivial feature to the Camlex: reverse engineering. Details can be found here: Camlex.NET became bidirectional and goes online. Version 3.0 is released. Briefly it allows to parse existing string query into expression tree. Based on this feature we ran online service: http://camlex-online.org where you can paste string CAML query and see how it looks like with Camlex syntax.

When worked over reverse engineering, I thought that there may be many interesting applications. Today I released version 3.2 with another feature built on top of reverse engineering which is closer to applied development. Now you may pass existing string query to the Camlex and add more conditions via lambdas. I.e. you may use Camlex with tools which work with plain CAML queries, expand them using Camlex syntax and pass back to that tool.

Let’s see example. Suppose that we have a CAML query:

   1: <Where>
   2:   <Eq>
   3:     <FieldRef Name="Title" />
   4:     <Value Type="Text">test</Value>
   5:   </Eq>
   6: </Where>

it will return list items which have Title = “test”. We need to add another condition: we also want to return those items which have Title = “foo”. Now we can make it like this:

   1: string existingQuery =
   2:     "   <Where>" +
   3:     "       <Eq>" +
   4:     "           <FieldRef Name=\"Title\" />" +
   5:     "           <Value Type=\"Text\">test</Value>" +
   6:     "       </Eq>" +
   7:     "   </Where>";
   8:  
   9: var query = Camlex.Query().WhereAny(existingQuery, 
  10:     x => (string)x["Title"] == "foo").ToString();

It will produce the following result:

   1: <Where>
   2:   <Or>
   3:     <Eq>
   4:       <FieldRef Name="Title" />
   5:       <Value Type="Text">foo</Value>
   6:     </Eq>
   7:     <Eq>
   8:       <FieldRef Name="Title" />
   9:       <Value Type="Text">test</Value>
  10:     </Eq>
  11:   </Or>
  12: </Where>

By the same way you may combine conditions via WhereAll method – and in CAML you will get “And” instead of “Or”. Of course you may pass complicated queries and combine several other expressions. Camlex will parse string, create resulting expression tree and returns combined CAML:

   1: string existingQuery =
   2:     "<Where>" +
   3:     "  <And>" +
   4:     "    <Eq>" +
   5:     "      <FieldRef Name=\"Title\" />" +
   6:     "      <Value Type=\"Text\">foo</Value>" +
   7:     "    </Eq>" +
   8:     "    <Eq>" +
   9:     "      <FieldRef Name=\"Title\" />" +
  10:     "      <Value Type=\"Text\">test</Value>" +
  11:     "    </Eq>" +
  12:     "  </And>" +
  13:     "</Where>";
  14:  
  15: var query = Camlex.Query().WhereAny(existingQuery,
  16:     x => (int)x["Count"] > 1 && x["Status"] != null).ToString();

will produce:

   1: <Where>
   2:   <Or>
   3:     <And>
   4:       <Gt>
   5:         <FieldRef Name="Count" />
   6:         <Value Type="Integer">1</Value>
   7:       </Gt>
   8:       <IsNotNull>
   9:         <FieldRef Name="Status" />
  10:       </IsNotNull>
  11:     </And>
  12:     <And>
  13:       <Eq>
  14:         <FieldRef Name="Title" />
  15:         <Value Type="Text">foo</Value>
  16:       </Eq>
  17:       <Eq>
  18:         <FieldRef Name="Title" />
  19:         <Value Type="Text">test</Value>
  20:       </Eq>
  21:     </And>
  22:   </Or>
  23: </Where>

By the same way you may expand OrderBy, GroupBy and ViewFields fields:

   1: string existingQuery =
   2:     "  <OrderBy>" +
   3:     "    <FieldRef Name=\"Modified\" Ascending=\"False\" />" +
   4:     "  </OrderBy>";
   5:  
   6: var query = Camlex.Query().OrderBy(existingQuery,
   7:     x => new[]{x["Title"], x["State"] as Camlex.Asc}).ToString();

it will get the following CAML:

   1: <OrderBy>
   2:   <FieldRef Name="Modified" Ascending="False" />
   3:   <FieldRef Name="Title" />
   4:   <FieldRef Name="State" Ascending="True" />
   5: </OrderBy>

This feature came from community and we also glad to hear your proposals on codeplex site. Hope new feature will be useful and will simplify your work.