Second Life of a Hungarian SharePoint Geek

March 5, 2013

Deleting files from the IE cache as part of the Visual Studio deployment process

Filed under: CKS.Dev, Internet Explorer, SP 2010, Visual Studio, VSX — Tags: , , , , — Peter Holpar @ 21:44

In the past months I had again a lot to do with client side SharePoint development, that means in practice mainly projects including ribbon extensions and tons of JavaScript files. One of the issues with that type of development is the bad habit of Internet Explorer called caching. For example, when you make modifications to the .js files, and re-deploy your project, it is rather common, that IE executes the former versions of the scripts, that I find pretty annoying.

Some of the workarounds I found for that issue in the past years:

Option Nr.1: Disable caching in IE (Internet options / General / Browsing history / Settings). It is OK if you use your environment only for development (like a Dev-VM), but not optimal if you should use the same browser to fulfill your other daily tasks.

image

Option Nr.2: Type the full URL of the script in the address text box of IE, download a local copy of the script to a temporary folder, and refresh the page with Ctrl+F5. Rather cumbersome method, especially if you have to refresh several scripts .

Option Nr.3: Open the location of the cache (Internet options / General / Browsing history / Settings / View files, marked with blue on the screenshot above), and delete the file(s) manually. The location in the file system is typically C:\Users\UserName\AppData\Local\Microsoft\Windows\Temporary Internet Files. Works quite good, but it takes some time and is still something you can forget.

Wouldn’t it be more comfortable to automate the process, for example, as a deployment step in Visual Studio? Definitely, but how to achieve that? Well, deleting files from the IE cache programmatically is far from being straightforward, but fortunately I found a nice example on MSDN for a sample wrapper class in C#, including a lot of PInvoke calls. Having this solution, the Visual Studio Extensibility part of the exercise was a routine task.

My first idea was to contribute this VS extension to the CKS.Dev project on CodePlex (see some posts on my other contributions to CKS.Dev here), but the project is just being upgraded to SharePoint 2013 / Visual Studio 2012, so I extended my own extension (documented here) instead.

Expected functionality:

I would like to specify through Visual Studio project properties, which files should be deleted from the cache (e.g. from the Temporary Internet Files folder).

First, I introduce a new property called IE cache delete rule that determines the scope of the action and has the following possible values:

Only from current site – only matching files cached from the active SharePoint site (the one you specified as the target of the deployment in VS) are deleted.

All from current sites – all files cached from the active SharePoint site are deleted, other filters are ignored.

No site specific – all cached files that fulfill the filters are deleted, independently from the origin URL.

Filters (there is a logical OR between the filters that means a cached file should fulfill at least one of the conditions to be deleted):

IE cache file list – It is a list of the names of the files to be deleted from the cache.

IE cache file pattern – It is a regular expression pattern. All files matching the pattern will be deleted from the cache, as long as it also matches the scope of the action (see IE cache delete rule property above)

Let’s have a look at the implementation!

My original SPVSProjectProps class was extended with the new properties:

Code Snippet
  1. // IECacheFilePattern property related members
  2. private const string IECacheFilePatternPropertyId = "IECacheFilePattern";
  3. private const string IECacheFilePatternPropertyDefaultValue = "";
  4.  
  5. [DisplayName("IE cache file pattern")]
  6. [DescriptionAttribute("This property specifies a regular expression pattern to determine which files should be deleted from the IE cache when the Clear IE cache deployment step is activated")]
  7. [DefaultValue(IECacheFilePatternPropertyDefaultValue)]
  8. // we want our custom property to show up in the SharePoint section in the project properties property grid
  9. [Category("SharePoint")]
  10. public string IECacheFilePattern
  11. {
  12.     get
  13.     {
  14.         string propertyValue;
  15.         int hr = projectStorage.GetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
  16.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
  17.  
  18.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  19.         if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
  20.         {
  21.             propertyValue = IECacheFilePatternPropertyDefaultValue;
  22.         }
  23.  
  24.         return propertyValue;
  25.     }
  26.  
  27.     set
  28.     {
  29.         projectStorage.SetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
  30.             (uint)_PersistStorageType.PST_PROJECT_FILE, value);
  31.     }
  32. }
  33.  
  34. // IECacheFileList property related members
  35. private const string IECacheFileListPropertyId = "IECacheFileList";
  36. private const string IECacheFileListPropertyDefaultValue = "";
  37.  
  38. [DisplayName("IE cache file list")]
  39. [Description("This property specifies which files should be deleted from the IE cache when the Clear IE cache deployment step is activated")]
  40. [DefaultValue(RelatedTimerJobsPropertyDefaultValue)]
  41. // we want our custom property to show up in the SharePoint section in the project properties property grid
  42. [Category("SharePoint")]
  43. // use custom property editor to avoid "Constructor on type 'System.String' not found." error in design mode
  44. [Editor("System.Windows.Forms.Design.StringArrayEditor, System.Design", typeof(UITypeEditor))]
  45. [TypeConverter(typeof(CsvArrayConverter))]
  46. public String[] IECacheFileList
  47. {
  48.     get
  49.     {
  50.         String propertyValue;
  51.         int hr = projectStorage.GetPropertyValue(IECacheFileListPropertyId, string.Empty,
  52.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
  53.  
  54.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  55.         if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
  56.         {
  57.             propertyValue = IECacheFileListPropertyDefaultValue;
  58.         }
  59.  
  60.         // remove accidental whitespaces
  61.         String[] fileNames = propertyValue.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
  62.         fileNames = Array.ConvertAll<String, String>(fileNames, fileName => fileName.Trim());
  63.         return fileNames;
  64.     }
  65.  
  66.     set
  67.     {
  68.         String propertyValue =
  69.             (value == null) ?
  70.             String.Empty :
  71.             // remove accidental whitespaces
  72.             String.Join("|", Array.ConvertAll<String, String>(value, fileName => fileName.Trim()));
  73.         projectStorage.SetPropertyValue(IECacheFileListPropertyId, string.Empty,
  74.             (uint)_PersistStorageType.PST_PROJECT_FILE, propertyValue);
  75.     }
  76. }
  77.  
  78. // IECacheDeleteRule property related members
  79. private const string IECacheDeleteRulePropertyId = "IECacheDeleteRule";
  80. private const IECacheDeleteRules IECacheDeleteRulePropertyDefaultValue = IECacheDeleteRules.OnlyCurrentSite;
  81.  
  82. [DisplayName("IE cache delete rule")]
  83. [Description("This property specifies the relation between the current SharePoint site and files to be deleted from IE cache")]
  84. [DefaultValue(IECacheDeleteRulePropertyDefaultValue)]
  85. // we want our custom property to show up in the SharePoint section in the project properties property grid
  86. [Category("SharePoint")]
  87. [TypeConverter(typeof(IECacheDeleteRuleConverter))]
  88. public IECacheDeleteRules IECacheDeleteRule
  89. {
  90.     get
  91.     {
  92.         // set default value
  93.         IECacheDeleteRules propertyValue = IECacheDeleteRulePropertyDefaultValue;
  94.         string propertyValueString;
  95.         int hr = projectStorage.GetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
  96.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValueString);
  97.  
  98.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  99.         if (ErrorHandler.Succeeded(hr) && !String.IsNullOrEmpty(propertyValueString))
  100.         {
  101.             Enum.TryParse<IECacheDeleteRules>(propertyValueString, out propertyValue);
  102.         }
  103.  
  104.         return propertyValue;
  105.     }
  106.  
  107.     set
  108.     {
  109.         projectStorage.SetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
  110.             (uint)_PersistStorageType.PST_PROJECT_FILE, value.ToString());
  111.     }
  112. }
  113.  
  114. public enum IECacheDeleteRules
  115. {
  116.     [Description("Only from current site")]
  117.     OnlyCurrentSite,
  118.     [Description("All from current site")]
  119.     AllFromCurrentSite,
  120.     [Description("No site specific")]
  121.     NoSiteSpecific
  122. }
  123.  
  124. // based on EnumConverter example from
  125. // http://www.c-sharpcorner.com/uploadfile/witnes/using-propertygrid-in-net/
  126. class IECacheDeleteRuleConverter : EnumConverter
  127. {
  128.     private Type enumType;
  129.  
  130.     public IECacheDeleteRuleConverter(Type type) : base(type)
  131.     {
  132.         enumType = type;
  133.     }
  134.  
  135.     public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
  136.     {
  137.         return destType == typeof(string);
  138.     }
  139.  
  140.     public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType)
  141.     {
  142.         FieldInfo fi = enumType.GetField(Enum.GetName(enumType, value));
  143.         DescriptionAttribute dna = (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
  144.         if (dna != null)
  145.             return dna.Description;
  146.         else
  147.             return value.ToString();
  148.     }
  149.  
  150.     public override bool CanConvertFrom(ITypeDescriptorContext context, Type srcType)
  151.     {
  152.         return srcType == typeof(string);
  153.     }
  154.  
  155.     public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  156.     {
  157.         foreach (FieldInfo fi in enumType.GetFields())
  158.         {
  159.             DescriptionAttribute dna =
  160.             (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
  161.             if ((dna != null) && ((string)value == dna.Description))
  162.                 return Enum.Parse(enumType, fi.Name);
  163.         }
  164.         return Enum.Parse(enumType, (string)value);
  165.     }
  166. }

We have two new methods to the static ExtensionHelper class. The ShouldDeleteIECacheFile method encapsulates the logic of cache file deletion as function of our new project properties. The ClearIECacheFile method calls the core cache file deletion (see ClearIEFiles method of the DeleteIECache class below) with this logic injected as a parameter. We do some logging in both of these methods to inform users in the Output window of VS about the progress and result of the deployment. We should keep the list of deleted files in the local filesDeleted variable, otherwise each files would be listed twice due to the logic implemented in the ClearIEFiles method.

Code Snippet
  1. internal static void ClearIECacheFile(ISharePointProject project)
  2. {
  3.     try
  4.     {
  5.         LogToOutputWindows(project, "Clearing files from IE cache");
  6.         SPVSProjectProps propertiesObject;
  7.         if (project.Annotations.TryGetValue<SPVSProjectProps>(out propertiesObject))
  8.         {
  9.             String ieCacheFilePattern = propertiesObject.IECacheFilePattern;
  10.             IEnumerable<String> ieCacheFileList = propertiesObject.IECacheFileList;
  11.             SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule = propertiesObject.IECacheDeleteRule;
  12.             List<String> filesDeleted = new List<String>();
  13.  
  14.             Func<Uri, bool> shouldDelete = new Func<Uri, bool>(u => ShouldDeleteIECacheFile(u,
  15.                                                                                         ieCacheFilePattern,
  16.                                                                                         ieCacheFileList,
  17.                                                                                         ieCacheDeleteRule,
  18.                                                                                         filesDeleted,
  19.                                                                                         project));
  20.             DeleteIECache.ClearIEFiles(shouldDelete);
  21.  
  22.             LogToOutputWindows(project, String.Format("Number of files deleted from IE cache: {0}", filesDeleted.Count));
  23.         }
  24.     }
  25.     catch (Exception ex)
  26.     {
  27.         LogToOutputWindows(project, String.Format("Clearing files from IE cache failed. Exception: {0}", ex.Message));
  28.     }
  29. }
  30.  
  31. internal static bool ShouldDeleteIECacheFile(Uri uri, String filePattern, IEnumerable<String> fileList,
  32.     SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule, List<String> filesDeleted, ISharePointProject project)
  33. {
  34.     bool result = false;
  35.  
  36.     Uri siteUrl = project.SiteUrl;
  37.     Regex filePatternRegex = string.IsNullOrEmpty(filePattern) ? null : new Regex(filePattern);
  38.     List<string> fileListEx = (fileList == null) ? new List<string>() : fileList.ToList();
  39.     string fileName = uri.Segments[uri.Segments.Length – 1];
  40.  
  41.     bool isFromCurrentSite = uri.AbsoluteUri.IndexOf(siteUrl.AbsoluteUri, StringComparison.InvariantCultureIgnoreCase) == 0;
  42.     if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.AllFromCurrentSite)
  43.     {
  44.         result = isFromCurrentSite;
  45.     }
  46.     else
  47.     {
  48.         result = ((fileListEx.Any(f => (f.ToUpper() == fileName.ToUpper()))) ||
  49.                 (filePatternRegex != null) && (filePatternRegex.IsMatch(fileName)));
  50.         if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.OnlyCurrentSite)
  51.         {
  52.             result = result && isFromCurrentSite;
  53.         }
  54.     }
  55.  
  56.     if ((result) && (!filesDeleted.Contains(uri.AbsoluteUri)))
  57.     {
  58.         filesDeleted.Add(uri.AbsoluteUri);
  59.         LogToOutputWindows(project, String.Format("Deleting file from IE cache: '{0}'", uri));
  60.     }
  61.  
  62.     return result;
  63. }

I kept the DeleteIECache class and its ClearIEFiles method as it was published on MSDN, except this method has a Func<Uri, bool> shouldDelete parameter that we use to decide if a specific file should be deleted from the IE cache:

returnValue = shouldDelete(uri) ? DeleteUrlCacheEntry(internetCacheEntry.lpszSourceUrlName) : false;

The code for the deployment step is pretty straightforward, it simply calls the ClearIECacheFile method of our ExtensionHelper class. Since we don’t have to call any SharePoint-specific code on the server side (that means no x64 process), there is no need for SharePoint commands in this case. That makes our life easier.

Code Snippet
  1. [DeploymentStep("PHolpar.ClearIECache")]
  2. [Export(typeof(IDeploymentStep))]
  3. internal class ClearIECacheDeployStep : IDeploymentStep
  4. {
  5.  
  6.     public bool CanExecute(IDeploymentContext context)
  7.     {
  8.         return true;
  9.     }
  10.  
  11.     public void Execute(IDeploymentContext context)
  12.     {
  13.         ISharePointProject project = context.Project;
  14.         ExtensionHelper.ClearIECacheFile(project);
  15.     }
  16.  
  17.     public void Initialize(IDeploymentStepInfo stepInfo)
  18.     {
  19.         stepInfo.Name = "Clear IE cache";
  20.         stepInfo.Description = "This step deletes the specified files from the Internet Explorer cache of the current user.";
  21.         stepInfo.StatusBarMessage = "IE cache is being cleared…";
  22.     }
  23. }

To test the new extension, I’ve created a deployment configuration called Clear IE cache.

image

This DC has only a single deployment step, our new Clear IE cache DS.

image

Using the following project properties (reg. exp. used: ^.*\.(js|JS)$) we can delete all .js files that were cached from the active SharePoint site:

image

We can specify the files to be deleted explicitly in the IE cache file list property:

image

In this case these files would be deleted from the cache independently from the URL they were downloaded from:

image

The next screenshot displays a sample deployment for the previous configuration:

image

Using the All from current sites value of IE cache delete rule (not illustrated here) the deployment process clears all files cached from the active SharePoint site, filtering properties (IE cache file list and IE cache file pattern) are ignored.

You can download the updated version (v 1.1) of my VS extension from the original location.

May 20, 2010

Visual Studio 2010 extension for helping SharePoint 2010 debugging

Filed under: Reflection, SP 2010, VSX — Tags: , , — Peter Holpar @ 01:03

Visual Studio 2010 has – of course, among others – a great new feature that is VSIX and MEF-based extensions. Built on that technology you can extend the out of the box SharePoint toolset in Visual Studio.

There is a lot of useful resources on the theory and practice of VS SharePoint tools extensions, so I will not cover it now. If you need the background, you can  easily pick that up from these sites:

SharePoint Development in Visual Studio 2010 (Visual Studio 2010 Walkthroughs)
SharePoint Development in Visual Studio
Visual Studio Software Development Kit (SDK)
Several posts from Waldek Mastykarz on VS 2010
Visual Studio 2010: My very first SharePoint project extension from Johan Leino
Code and walkthrough for my Visual Studio Extensibility session from Wouter van Vugt

In this post I would like to introduce an extension I’ve created to make my life a bit easier. These tools are always “under construction”, but I’ve tried to create a consistent release to be able to share it with fellow developers.

I have published the project on CodePlex here. If you want to build it yourself, don’t forget to install the Visual Studio 2010 SDK.

The extension adds six new menu items to the SharePoint project menu:

Attach debugger to OWSTIMER: If you would like to debug timer jobs, e-mail event receivers or other code that runs in the context of the SharePoint Timer Service, you can click this menu item instead of bothering with the Attach to Process dialog box (Debug / Attach to Process…).

Attach debugger to VSSPHOST4: The vssphost4.exe process is responsible for running SharePoint commands started by Visual Studio 2010. If your code runs in this context, that is the case for example when you create feature event receivers, and would like to debug that code, you can use this menu. In the current version the tool assumes that the process is already running. If this assumption is false, the action will fail.

Attach debugger to W3WP: Although Visual Studio attaches the debugger to the current web application pool process if you start and deploy the project, but deployment sometimes takes extra time (for example, the code did not change and already deployed) and would be easier just to simply attach to the worker process.

Recycle W3WP: Yes, there is a similar deployment step that recycle the current application pool process, but as I wrote, I don’t want to always deploy my project. This menu item helps me in this case.

Restart OWSTIMER: The reason for that is similar than the former one.

Start timer jobs: If you want to debug your timer jobs, you should start them first. You can wait for the jobs to start by their schedule or start them manually one-by-one in Central Admin, but it would be the best if you were able to start the from the Visual Studio IDE, where you want to debug them. So I extended the SharePoint projects with a custom property where you can specify the jobs you want to restart frequently when working on the project. This menu item will try to restart the jobs for you.

image

The following is an output of the job restart. You can see that there is a job that does not exist in my system. In this case you get an alert. Similarly, if the job is already in the running state, you get another kind of warning.

image

In the project extension I add new menu items and properties to the project:

  1. public void Initialize(ISharePointProjectService projectService)
  2. {
  3.     _ps = projectService;
  4.     _ps.ProjectMenuItemsRequested += new EventHandler<SharePointProjectMenuItemsRequestedEventArgs>(ProjectMenuItemsRequested);
  5.     _ps.ProjectPropertiesRequested += new EventHandler<SharePointProjectPropertiesRequestedEventArgs>(ProjectPropertiesRequested);
  6. }
  7.  
  8. // adding custom properties
  9. void ProjectPropertiesRequested(object sender, SharePointProjectPropertiesRequestedEventArgs e)
  10. {
  11.     SPVSProjectProps propertiesObject;
  12.  
  13.     // If the properties object already exists, get it from the project's annotations.
  14.     if (!e.Project.Annotations.TryGetValue<SPVSProjectProps>(out propertiesObject))
  15.     {
  16.         // Otherwise, create a new properties object and add it to the annotations.
  17.         propertiesObject = new SPVSProjectProps(e.Project);
  18.         e.Project.Annotations.Add(propertiesObject);
  19.     }
  20.  
  21.     e.PropertySources.Add(propertiesObject);
  22. }
  23.  
  24. // adding custom project menus
  25. void ProjectMenuItemsRequested(object sender, SharePointProjectMenuItemsRequestedEventArgs e)
  26. {
  27.     IMenuItem miAttachToW3wp = e.ActionMenuItems.Add("Attach debugger to W3WP");
  28.     miAttachToW3wp.Click += new EventHandler<MenuItemEventArgs>(miAttachToW3wp_Click);
  29.     IMenuItem miAttachToOwsTimer = e.ActionMenuItems.Add("Attach debugger to OWSTIMER");
  30.     miAttachToOwsTimer.Click += new EventHandler<MenuItemEventArgs>(miAttachToOwsTimer_Click);
  31.     IMenuItem miRestartVsspHost = e.ActionMenuItems.Add("Attach debugger to VSSPHOST4");
  32.     miRestartVsspHost.Click += new EventHandler<MenuItemEventArgs>(miAttachToVsspHost_Click);
  33.     IMenuItem miRecycleW3wp = e.ActionMenuItems.Add("Recycle W3WP");
  34.     miRecycleW3wp.Click += new EventHandler<MenuItemEventArgs>(miRecycleW3wp_Click);
  35.     IMenuItem miRestartOwsTimer = e.ActionMenuItems.Add("Restart OWSTIMER");
  36.     miRestartOwsTimer.Click += new EventHandler<MenuItemEventArgs>(miRestartOwsTimer_Click);
  37.     IMenuItem miRestartTimerJob = e.ActionMenuItems.Add("Start timer jobs");
  38.     miRestartTimerJob.Click += new EventHandler<MenuItemEventArgs>(miRestartTimerJob_Click);
  39. }

As you can see, the menu items call forward into methods defined in helper classes:

  1. void miRestartTimerJob_Click(object sender, MenuItemEventArgs e)
  2. {
  3.     ISharePointProject project = ((ISharePointProject)e.Owner);
  4.  
  5.     ExtensionHelper.RestartTimerJobs(project);
  6. }
  7.  
  8. void miRestartOwsTimer_Click(object sender, MenuItemEventArgs e)
  9. {
  10.     LogToOutputWindows("Restarting OWSTIMER service…");
  11.     if (ExtensionHelper.RestartService("SPTimerV4", 10))
  12.     {
  13.         LogToOutputWindows("OWSTIMER service restarted");
  14.     }
  15.     else
  16.     {
  17.         LogToOutputWindows("Restarting OWSTIMER service failed");
  18.     }
  19. }
  20.        
  21. void miAttachToW3wp_Click(object sender, MenuItemEventArgs e)
  22. {
  23.     ISharePointProject project = ((ISharePointProject)e.Owner);
  24.     ISharePointConnection connection = project.SharePointConnection;
  25.     EnvDTE.Project dteProj = _ps.Convert<ISharePointProject, EnvDTE.Project>(project);
  26.     AttachProcessHelper apHelpers = new AttachProcessHelper(dteProj.DTE);
  27.  
  28.     // get the URL of the project SharePoint site just for logging
  29.     String siteUrl = project.SiteUrl.AbsoluteUri;
  30.     LogToOutputWindows("Site URL: " + siteUrl);
  31.     // get the application pool name from the current context
  32.     String appPoolName = connection.ExecuteCommand<String>("PHolpar.GetAppPoolName");
  33.     LogToOutputWindows("Application pool name: " + appPoolName);
  34.     // get the process ID of the application pool
  35.     uint appPoolProcId = apHelpers.GetProcessIdByAppPoolName(appPoolName);
  36.     LogToOutputWindows("Application pool process ID: " + appPoolProcId);
  37.     // try to attach to the process
  38.     LogToOutputWindows("Trying to attach to W3WP process");
  39.     if (apHelpers.AttachToProcessById(appPoolProcId))
  40.     {
  41.         LogToOutputWindows("Debugger attached to W3WP process");
  42.     }
  43.     else
  44.     {
  45.         LogToOutputWindows("Failed to attach to W3WP process");
  46.     }
  47. }
  48.  
  49. void miAttachToOwsTimer_Click(object sender, MenuItemEventArgs e)
  50. {
  51.     ISharePointProject project = ((ISharePointProject)e.Owner);
  52.     ExtensionHelper.AttachToProcessByName(project, "owstimer.exe");
  53. }
  54.  
  55. void miAttachToVsspHost_Click(object sender, MenuItemEventArgs e)
  56. {
  57.     ISharePointProject project = ((ISharePointProject)e.Owner);
  58.     ExtensionHelper.AttachToProcessByName(project, "vssphost4.exe");
  59. }
  60.  
  61. void miRecycleW3wp_Click(object sender, MenuItemEventArgs e)
  62. {
  63.     ISharePointProject project = ((ISharePointProject)e.Owner);
  64.     ExtensionHelper.RecycleW3wp(project);            
  65. }

We use DTE to attach processes. The AttachProcessHelper class encapsulates the bulk of the functionality. We can attach to the process by the ID or by the name of the process.

  1. internal class AttachProcessHelper
  2. {
  3.     private DTE _dte;
  4.  
  5.     public AttachProcessHelper(DTE dte)
  6.     {
  7.         _dte = dte;
  8.     }
  9.  
  10.     internal bool AttachToProcessByName(String processName)
  11.     {
  12.         Debugger dbg = _dte.Debugger;
  13.         foreach (EnvDTE80.Process2 process in dbg.LocalProcesses)
  14.         {
  15.             if (process.Name.ToUpper().LastIndexOf(processName.ToUpper()) == (process.Name.Length – processName.Length))
  16.             {
  17.                 process.Attach();
  18.                 return true;
  19.             }
  20.         }
  21.         return false;
  22.     }
  23.  
  24.     internal bool AttachToProcessById(long processId)
  25.     {
  26.         Debugger dbg = _dte.Debugger;
  27.  
  28.         foreach (EnvDTE80.Process2 process in dbg.LocalProcesses)
  29.         {
  30.             if (process.ProcessID == processId)
  31.             {
  32.                 process.Attach();
  33.                 return true;
  34.             }
  35.         }
  36.         return false;
  37.     }
  38.  
  39.     internal uint GetProcessIdByAppPoolName(String appPoolName)
  40.     {
  41.         uint result = 0;
  42.         ManagementScope scope = new ManagementScope(@"\\localhost\root\cimv2");
  43.         ManagementObjectSearcher searcher = new ManagementObjectSearcher("select * from Win32_Process where Name='w3wp.exe'");
  44.         searcher.Scope = scope;
  45.         foreach (ManagementObject process in searcher.Get())
  46.         {
  47.             String commandLine = (String)process.GetPropertyValue("CommandLine");
  48.             String appPoolNameFromCommandLine = GetAppPoolNameFromCommandLine(commandLine);
  49.             if (appPoolNameFromCommandLine.ToUpper() == appPoolName.ToUpper())
  50.             {
  51.                 result = (uint)process.GetPropertyValue("ProcessId");
  52.                 break;
  53.             }
  54.         }
  55.         return result;
  56.     }
  57.  
  58.     internal String GetAppPoolNameFromCommandLine(string commandLine)
  59.     {
  60.         String result = String.Empty;
  61.         Regex re = new Regex(@"-ap ""(.+)"" -v", RegexOptions.IgnoreCase);
  62.         MatchCollection matches = re.Matches(commandLine);
  63.         if (matches.Count == 1)
  64.         {
  65.             if (matches[0].Groups.Count > 1)
  66.             {
  67.                 result = matches[0].Groups[1].Value;
  68.             }
  69.         }
  70.         return result;
  71.     }
  72.  
  73. }

There is an additional layer of abstraction in the static ExtensionHelper class:

  1. internal static void AttachToProcessByName(ISharePointProject project, String processName)
  2. {
  3.     EnvDTE.Project dteProj = project.ProjectService.Convert<ISharePointProject, EnvDTE.Project>(project);
  4.     LogToOutputWindows(project, String.Format("Trying to attach to {0} process", processName));
  5.     AttachProcessHelper apHelpers = new AttachProcessHelper(dteProj.DTE);
  6.     if (apHelpers.AttachToProcessByName(processName))
  7.     {
  8.         LogToOutputWindows(project, String.Format("Debugger attached to {0} process", processName));
  9.     }
  10.     else
  11.     {
  12.         LogToOutputWindows(project, String.Format("Failed to attach to {0} process", processName));
  13.     }
  14. }

We get the name of the current application pool by the following SharePoint command:

  1. [SharePointCommand("PHolpar.GetAppPoolName")]
  2. public static String GetAppPoolName(ISharePointCommandContext context)
  3. {
  4.     SPSite site = context.Site;
  5.     SPWebApplication webApp = site.WebApplication;
  6.     SPApplicationPool appPool = webApp.ApplicationPool;                
  7.     return appPool.Name;
  8. }

The RestartService method is used to restart the specified service, in this case the SharePoint timer. You can see from the miRestartOwsTimer_Click introduced above that this method is called with the “SPTimerV4” as service name (that is the internal name of the timer service in SharePoint v4) and 10 seconds as the hard-coded timeout to service stop and start.

  1. internal static bool RestartService(String serviceName, int timeOut)
  2. {
  3.     try
  4.     {
  5.         TimeSpan timeOutSpan = TimeSpan.FromSeconds(timeOut);
  6.         ServiceController sc = new ServiceController(serviceName);
  7.         if (sc.Status == ServiceControllerStatus.Running)
  8.         {
  9.             sc.Stop();
  10.             sc.WaitForStatus(ServiceControllerStatus.Stopped, timeOutSpan);
  11.         }
  12.         sc.Start();
  13.         sc.WaitForStatus(ServiceControllerStatus.Running, timeOutSpan);
  14.         return true;
  15.     }
  16.     catch
  17.     {
  18.         return false;
  19.     }
  20. }

The RecycleW3wp method is a bit more tricky. Since there is already a built-in method in a deployment step to recycle the application pool for a project, I re-used that internal method using Reflection.

  1. internal static void RecycleW3wp(ISharePointProject project)
  2. {
  3.     try
  4.     {
  5.         // hack to get the private Microsoft.VisualStudio.SharePoint.Project assembly
  6.         Assembly privateSPProjAssembly = typeof(VSProjectPackage).Assembly;
  7.         // DeploymentUtils is an internal static classes,
  8.         // so you cannot get them directly from code            
  9.         Type deploymentUtilsType = privateSPProjAssembly.GetType("Microsoft.VisualStudio.SharePoint.Deployment.DeploymentUtils");
  10.  
  11.         // let's call the
  12.         // internal static void RecycleAppPool(ISharePointProject project)
  13.         // method of DeploymentUtils
  14.         MethodInfo mi_RecycleAppPool = deploymentUtilsType.GetMethod("RecycleAppPool",
  15.                 BindingFlags.NonPublic | BindingFlags.Static);
  16.         if (mi_RecycleAppPool != null)
  17.         {
  18.             // call the static method with the project parameter
  19.             mi_RecycleAppPool.Invoke(null, new object[1] { project });
  20.             LogToOutputWindows(project, "IIS application pool recycled");
  21.         }
  22.         else
  23.         {
  24.             LogToOutputWindows(project, "Recycling IIS application pool failed.");
  25.         }
  26.     }
  27.     catch (Exception ex)
  28.     {
  29.         LogToOutputWindows(project, String.Format("Recycling IIS application pool failed. Exception: {0}", ex.Message));
  30.     }
  31. }

Timer jobs are restarted by the RestartTimerJobs method. This method gets the name of the related jobs using the custom project properties we will discuss soon.

  1. internal static void RestartTimerJobs(ISharePointProject project)
  2. {
  3.     LogToOutputWindows(project, "Restarting timer jobs");
  4.     ISharePointConnection connection = project.SharePointConnection;
  5.     SPVSProjectProps propertiesObject;
  6.  
  7.     if (project.Annotations.TryGetValue<SPVSProjectProps>(out propertiesObject))
  8.     {
  9.         List<String> relatedTimerJobs = new List<String>(propertiesObject.RelatedTimerJobs);
  10.         foreach (String relatedTimerJob in relatedTimerJobs)
  11.         {
  12.             LogToOutputWindows(project, String.Format("Restarting timer job: {0}", relatedTimerJob));
  13.             StartTimerJobResult result = connection.ExecuteCommand<String, StartTimerJobResult>("PHolpar.StartTimerJob", relatedTimerJob);
  14.             if (result.Succeeded)
  15.             {
  16.                 LogToOutputWindows(project, "Job started");
  17.             }
  18.             else
  19.             {
  20.                 LogToOutputWindows(project, result.ResponseMessage);
  21.             }
  22.         }
  23.     }

The restart takes place through the StartTimerJob command defined in a separate 64-bit project in the solution.

  1. [SharePointCommand("PHolpar.StartTimerJob")]
  2. public static StartTimerJobResult StartServiceJob(ISharePointCommandContext context, String jobName)
  3. {
  4.     SPSite site = context.Site;
  5.     return CommandHelper.StartServiceJob(site, jobName);
  6. }

It calls forward into static helper methods again.

  1. public static StartTimerJobResult StartServiceJob(SPSite site, String serviceTypeName, String jobName)
  2.         {
  3.             try
  4.             {
  5.                 foreach (SPJobDefinition jobDefinition in site.WebApplication.JobDefinitions)
  6.                 {
  7.                     Trace.TraceInformation("Job name: '{0}, title: {1}'", jobDefinition.Name, jobDefinition.Title);
  8.                     if ((jobDefinition.Name == jobName) || (jobDefinition.Title == jobName))
  9.                     {
  10.                         Trace.TraceInformation("Job found");
  11.                         if (!JobIsRunning(site.WebApplication.RunningJobs, jobDefinition))
  12.                         {
  13.                             Trace.TraceInformation("Job starting");
  14.                             jobDefinition.RunNow();
  15.                             return new StartTimerJobResult
  16.                                 {
  17.                                     Succeeded = true
  18.                                 };
  19.                         }
  20.                         else
  21.                         {
  22.                             Trace.TraceInformation("Job is already running, cannot be started");
  23.                             return new StartTimerJobResult
  24.                             {
  25.                                 ResponseMessage = "Job is already running, cannot be started",
  26.                                 Succeeded = false
  27.                             };
  28.                         }                      
  29.                     }
  30.                 }
  31.             }
  32.             catch (Exception ex)
  33.             {
  34.                 Trace.TraceError(String.Format("Error in StartServiceJob: '{0}'", ex.Message));
  35.                 return new StartTimerJobResult
  36.                 {
  37.                     ResponseMessage = String.Format("Error in StartServiceJob: '{0}'", ex.Message),
  38.                     Succeeded = false
  39.                 };
  40.             }
  41.             Trace.TraceInformation("Job not found");
  42.             return new StartTimerJobResult
  43.             {
  44.                 ResponseMessage = String.Format("Job not found"),
  45.                 Succeeded = false
  46.             };
  47.         }
  48.  
  49.         public static StartTimerJobResult StartServiceJob(SPSite site, String jobName)
  50.         {
  51.             return StartServiceJob(site, null, jobName);
  52.         }
  53.         
  54.         private static bool JobIsRunning(SPRunningJobCollection runningJobs, SPJobDefinition jobDefinition)
  55.         {
  56.             foreach (SPRunningJob runningJob in runningJobs)
  57.             {
  58.                 if (jobDefinition.Id == runningJob.JobDefinitionId)
  59.                 {
  60.                     return true;
  61.                 }
  62.             }
  63.             return false;
  64.         }

The above methods accepts the name or the title of the job to be started and returns the result using the following  class:

  1. [Serializable()]
  2. public class StartTimerJobResult
  3. {
  4.     public String ResponseMessage { get; set; }
  5.     public bool Succeeded { get; set; }
  6. }

The class is decorated with the Serializable attribute. I discussed that issue in my former post.

The following table shows some of the possible values for timer jobs probably installed on your system and scheduled frequently so consume not much resources and ideal for testing:

Job title

Job name

Immediate Alerts

job-immediate-alerts

Scheduled Unpublish

SchedulingUnpublish

Scheduled Approval 

SchedulingApproval

But let’s back to the above-mentioned custom project properties.

The following class define the custom property, stored in the project file:

  1. public class SPVSProjectProps
  2. {
  3. private ISharePointProject sharePointProject;
  4. private IVsBuildPropertyStorage projectStorage;
  5. private const string ProjectFilePropertyId = "RelatedTimerJobs";
  6. private const string ProjectFilePropertyDefaultValue = "";
  7.  
  8. public SPVSProjectProps(ISharePointProject myProject)
  9. {
  10.     sharePointProject = myProject;
  11.     projectStorage = sharePointProject.ProjectService.Convert<ISharePointProject, IVsBuildPropertyStorage>(sharePointProject);
  12. }
  13.  
  14. [DisplayName("Related timer jobs")]
  15. [Description("Name of the related timer jobs")]
  16. [DefaultValue(ProjectFilePropertyDefaultValue)]
  17. // we want our custom property to show up in the SharePoint section in the project properties property grid
  18. [Category("SharePoint")]
  19. // use custom property editor to avoid "Constructor on type 'System.String' not found." error in design mode
  20. [Editor("System.Windows.Forms.Design.StringArrayEditor, System.Design", typeof(UITypeEditor))]
  21. [TypeConverter(typeof(CsvArrayConverter))]
  22. public String[] RelatedTimerJobs
  23. {
  24.     get
  25.     {
  26.         String propertyValue;
  27.         int hr = projectStorage.GetPropertyValue(ProjectFilePropertyId, string.Empty,
  28.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
  29.  
  30.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  31.         if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
  32.         {
  33.             propertyValue = ProjectFilePropertyDefaultValue;
  34.         }
  35.  
  36.         // remove accidental whitespaces
  37.         String[] timerJobNames = propertyValue.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
  38.         timerJobNames = Array.ConvertAll<String, String>(timerJobNames, timerJobName => timerJobName.Trim());
  39.         return timerJobNames;
  40.     }
  41.  
  42.     set
  43.     {
  44.         String propertyValue =
  45.             (value == null) ?
  46.             String.Empty :
  47.             // remove accidental whitespaces
  48.             String.Join("|", Array.ConvertAll<String, String>(value, timerJobName => timerJobName.Trim()));
  49.         // Do not save the default value.
  50.         if (!String.IsNullOrEmpty(propertyValue))
  51.         {
  52.             projectStorage.SetPropertyValue(ProjectFilePropertyId, string.Empty,
  53.                 (uint)_PersistStorageType.PST_PROJECT_FILE, propertyValue);
  54.         }
  55.     }
  56. }

It is based on a String array and utilize a custom editor, displayed below.

image

We eliminate empty values and trim whitespace from the beginning and end of the strings.

We use a converter class to make the property display value more user friendly.

  1. class CsvArrayConverter : ArrayConverter
  2. {
  3.     public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
  4.     {
  5.         Array array = value as Array;
  6.         if (destinationType == typeof(string) && array != null)
  7.         {
  8.             StringBuilder sb = new StringBuilder();
  9.             for (int i = 0; i < array.Length; i++)
  10.             {
  11.                 if (sb.Length > 0)
  12.                 {
  13.                     sb.Append(", ");
  14.                 }
  15.                 sb.Append(array.GetValue(i));
  16.             }
  17.             return sb.ToString();
  18.  
  19.         }
  20.         return base.ConvertTo(context, culture, value, destinationType);
  21.     }
  22. }

image

The next figure shows the persisted settings in the project file:

image

The extension also adds three new deployment steps to the existing ones. These new steps are three of the above mentioned menu items. I feel the Attach debugger to VSSPHOST4 step useful when you work with feature event handlers and would like to capture the first activation of the feature that happens when the project is deployed by Visual Studio. In this case you should include this step in your custom deployment configuration before the Activate Features step.

image 

Since these steps utilize the same helper methods as the menu items do, there is nothing new in their code. I just show one of them to illustrate the structure of the deployment steps for those of you not very familiar with that:

  1. [DeploymentStep("PHolpar.AttachToVsspHost4")]
  2. [Export(typeof(IDeploymentStep))]
  3. internal class AttachDeployStep : IDeploymentStep
  4. {
  5.     public bool CanExecute(IDeploymentContext context)
  6.     {
  7.         return true;
  8.     }
  9.  
  10.     public void Execute(IDeploymentContext context)
  11.     {
  12.         ISharePointProject project = context.Project;
  13.         ExtensionHelper.AttachToProcessByName(project, "vssphost4.exe");
  14.     }
  15.  
  16.     public void Initialize(IDeploymentStepInfo stepInfo)
  17.     {
  18.         stepInfo.Name = "Attach debugger to VSSPHOST4";
  19.         stepInfo.Description = "This step attaches the debugger to the vssphost4 (if already running) to enable debugging of elements involved in the deployment process, for example, feature receivers.";
  20.         stepInfo.StatusBarMessage = "Attaching to vssphost4…";
  21.     }
  22. }

To get the full code and play with that visit the CodePlex site of the project.

May 18, 2010

Interesting registry settings to help debugging SharePoint Tools Extensions in Visual Studio 2010

Filed under: SP 2010, VSX — Tags: , — Peter Holpar @ 03:10

Today I found a page on MSDN about Debugging Extensions for the SharePoint Tools in Visual Studio. At the end of the page there is a table of some registry settings that can be used to change the behavior of Visual Studio and may help troubleshooting.

If you would like to use these settings, you have to create the DWORD values under the HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0\SharePointTools key.

For me probably the most exciting one is the AttachDebuggerToHostProcess that can be used to attach to the vssphost4.exe immediately after it starts.

I’ve check it and – as expected – it can be used not only for the custom extensions but the built in tools either. So, for example, it can be used to attach the debugger to the feature receiver process when deploying the feature from the Visual Studio deployment steps.

The following dialog box is raised on the deployment:

image

The details show that the reason for the prompt is really the injected Debugger.Break statement as documented.

image

You should choose the Debug the program option, and select the Visual Studio instance for debugging.

image

After selecting the Visual Studio instance that is just deploying the solution, it is attached to the host process as shown below:

image

Setting the value of EnableDiagnostics to 0 will disable displaying diagnostic messages in the Output window.

The ChannelOperationTimeout and the HostProcessStartupTimeout settings can be used to specify the timeout values Visual Studio waits for a command to execute or the host process (vssphost4.exe) to launch. Default values are 120 seconds and 60 seconds.

The last two documented settings, MaxReceivedMessageSize and MaxStringContentLength specify the maximum size of the  WCF messages and the strings (both in bytes with default of 1,048,576 bytes  = 1 MB) that allowed in the communication between Visual Studio and vssphost4.exe.

An important tip from the page for using the AttachDebuggerToHostProcess property:

“If you set this value to 1, you may also want to increase the HostProcessStartupTimeout value to give yourself enough time to attach the debugger before Visual Studio expects vssphost4.exe to signal that it started successfully.”

In my experience, if we wait to much the host process fails to start with the following error message:

Error occurred in deployment step ‘Add Solution’: The vssphost4.exe process was unable to start due to an unknown error or problem.

If you retry the deployment, you get no prompt for debugging, but an error message that states:

Error occurred in deployment step ‘Recycle IIS Application Pool’: The vssphost4.exe process was unable to start due to an unknown error or problem.

In this case the simplest solution is to restart Visual Studio.

SharePointConnectionException when executing a SharePoint command

Filed under: SP 2010, VSX — Tags: , — Peter Holpar @ 02:16

The other day I developed a custom SharePoint command in Visual Studio 2010.

I was to specify a String parameter for the command and it should have returned a simple .NET object having a few properties. Something like that:

  1. using System;
  2.  
  3. namespace MyNameSpace
  4. {
  5.     public class MyClass
  6.     {
  7.         public String Title { get; set; }
  8.         public bool IsDeleted { get; set; }
  9.     }
  10. }

To execute the command that returns this kind of data I had to use the following overload of the ExecuteCommand method:

TResult ExecuteCommand<T, TResult>(string commandId, T arg)

It seemed that the command was executed successfully, but I experienced that instead of the expected result, an exotic runtime exception was generated:

Microsoft.VisualStudio.SharePoint.SharePointConnectionException. Message: Type ‘MyNameSpace.MyClass’ with data contract name ‘MyClass:http://schemas.datacontract.org/2004/07/SPVSCommands’ is not expected. Add any types not known statically to the list of known types – for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.

I used this type of ExecuteMethod successfully for simple object types, like String.

The following posts gave me some info on the possible background of the error:

All About KnownTypes
About KnownTypeAttribute example

Finally, I found a rather simple and logic solution for the issue:

  1. using System;
  2. using System.Runtime.Serialization;
  3.  
  4. namespace MyNameSpace
  5. {
  6.     [Serializable()]
  7.     public class MyClass
  8.     {
  9.         public String Title { get; set; }
  10.         public bool IsDeleted { get; set; }
  11.     }
  12. }

It seems the object must be serializable to be transmitted over the wire. It is interesting that if I use a similar “complex” object type as the input parameter of the ExecuteMethod, it runs without error, even if the class is not serializable.

January 14, 2010

Why don’t you create your own code snippet library for SharePoint?

Filed under: SharePoint, VSX — Tags: , — Peter Holpar @ 01:28

Code snippets provide a simple yet effective tool in the VSX (Visual Studio Extensibility) that can save you from a lot of typing and help to code faster.

You can create your own code snippet library for SharePoint, either for yourself or for novice colleagues to help their work while mastering SharePoint development.

If you need general information about working and creation of code snippets, you will find a lot of useful content at the following links:

Code Snippets

Code Snippets Schema Reference

Creating and Using IntelliSense Code Snippets

You can create and edit code snippets using Notepad like me, but you can use any XML editor, and there is a dedicated tool as well, called Snippy.

I suggest you to create your snippets in a dedicated folder, for example, I’ve created my code snippet library in the C:\Program Files\Microsoft Visual Studio 9.0\VC#\Snippets\1033\SharePoint folder. You should register this location in Visual Studio using the Code Snippets Manager (Tools / Code Snippets Manager…) as illustrated below.

image

I’ll illustrate the process of creating a SharePoint code snippet library on a simple example that is one of the SharePoint best practices: the using clause in the Coding Techniques to Ensure Object Disposal.

We want our code to be surrounded by the following using clauses:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.       
  6.     }
  7. }

To achieve this result, I’ve created the following XML and saved it into the code snippet folder created earlier using the file name getweb.snippet.

  1. <?xml version="1.0" encoding="utf-8" ?>
  2.   <CodeSnippetsxmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  3.     <CodeSnippet Format="1.0.0">
  4.         <Header>
  5.             <Title>getweb</Title>
  6.             <Shortcut>getweb</Shortcut>
  7.             <Description>Code snippet for getting a web reference</Description>
  8.             <Author>Peter Holpar</Author>
  9.             <SnippetTypes>
  10.                 <SnippetType>SurroundsWith</SnippetType>
  11.             </SnippetTypes>
  12.         </Header>
  13.         <Snippet>
  14.             <Declarations>
  15.                 <Literal>
  16.                     <ID>siteIdentifier</ID>
  17.                     <ToolTip>Identifier that represents the SPSite object</ToolTip>
  18.                     <Default>site</Default>
  19.                 </Literal>
  20.                 <Literal>
  21.                     <ID>webIdentifier</ID>
  22.                     <ToolTip>Identifier that represents the SPWeb object</ToolTip>
  23.                     <Default>web</Default>
  24.                 </Literal>
  25.                 <Literal>
  26.                     <ID>url</ID>
  27.                     <ToolTip>URL of the site</ToolTip>
  28.                     <Default>url</Default>
  29.                 </Literal>
  30.             </Declarations>
  31.             <Code Language="csharp"><![CDATA[using (SPSite $siteIdentifier$ = new SPSite($url$))
  32. {
  33.     using (SPWeb $webIdentifier$ = $siteIdentifier$.OpenWeb())
  34.     {
  35.         $selected$ $end$
  36.     }
  37. }]]>
  38.             </Code>
  39.         </Snippet>
  40.     </CodeSnippet>
  41. </CodeSnippets>

Since we want our code snippet to surround existing code, we use the value SurroundsWith as SnippetType.

Let’s see our code snippet in action!

We create the bulk of our method as shown below.

  1. public void ProcessWeb(String url)
  2. {
  3.     web.Title = "MyWeb";
  4.     web.Update();
  5. }

Then we select the two code lines of the method, right-click on the selection, and choose Surround With… from the pop-up menu. Then we choose SharePoint as the code snippet library and the getweb code snippet.

image

The next picture shows the result after the code snippet is applied to the selection.

image

I hope that this sample was able to demonstrate the power of code snippets and you can use this tool to make your work more effective, as you will be able to concentrate on tasks that really require your IQ instead of bothering with typing the same code blocks over and over again.

Create a free website or blog at WordPress.com.