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.

Advertisements

October 6, 2011

Determining the path of the virtual directory for a SharePoint web application from Visual Studio

Filed under: CKS.Dev, SP 2010, Visual Studio — Tags: , , — Peter Holpar @ 11:41

The other day I was to add a minor extension to my customized version of CKS.Dev. If I select a deployable item (or a folder containing such items) in Solution Explorer, the extension should display a custom menu item that opens the deployment location using Windows Explorer.

It is similar to the standard Visual Studio 2010 menu item Open Folder in Windows Explorer, but instead of opening the original location, this menu should open the corresponding folder in the 14 hive (for items deployed to {SharePointRoot}) or in the InetPub the (for items deployed to {WebApplicationRoot}).

// BTW, I already added my own version of Open Folder in Windows Explorer to Visual Studio 2008 (and another one that opens the folder in the command shell) a few years ago and so was happy to welcome it built into VS 2010. Seems that other developers missed that feature as well.

It was easy to determine the root of the SharePoint installation (that is the 14 hive). Assuming you have a reference to an ISharePointProject (let’s call it project)  this path is available as project.ProjectService.SharePointInstallPath (the property is populated from the registry hive HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\14.0 with the Location value ). Actually, the current version of CKS.Dev uses exactly this way to get the path in the QuickDeploymentUtilities.SubstituteRootTokens static method. However, there is nothing that handles the {WebApplicationRoot} token.

So my first idea was to create a custom SharePoint command to retrieve the path of the virtual directory for the SharePoint application set for the current project in Visual Studio. But before doing so, I should have known how to get the value on the server side.

The path we need is available as a DirectoryInfo value in the Path property of the SPIisSettings class (Microsoft.SharePoint.Administration namespace, Microsoft.SharePoint assembly). To get the SPIisSettings instance corresponding to our web application we could use either the IisSettings property or the GetIisSettingsWithFallback method of the SPWebApplication class. In both cases we should specify the zone we need info about.

But after analyzing the Path property using Reflector it turned out that Microsoft has already created the command we need: the GetWebApplicationLocalPath method can be found in the SharePointCommands class (Microsoft.VisualStudio.SharePoint.Commands namespace, Microsoft.VisualStudio.SharePoint.Commands.Implementation.v4 assembly in GAC).

Note: If you check the SharePointCommands class you can see there are a lot of useful commands implemented in this class, and there are many more in other (usually not public) classes provided by Visual Studio 2010. I spent a few days last year with discovering the possibilities of these commands and I can assure you there are a few real gems among them. I’ve been planning a series of blog posts about this topic since then, but unfortunately I missed the time to publish them at the level I would like to do. So might be I will publish them in a kind of raw format to help your work. I think if you are at least aware of the existence of these commands it is a large step into the right direction.

Having the GetWebApplicationLocalPath method found (that is the Microsoft.VisualStudio.SharePoint.Commands.GetWebApplicationLocalPath command) it is quite trivial to get the path of the web application root:

String webApplicationRoot = project.SharePointConnection.ExecuteCommand<String>("Microsoft.VisualStudio.SharePoint.Commands.GetWebApplicationLocalPath");

September 21, 2011

Strong name verification issues when creating a customized build of the CKS.Dev solution

Filed under: CKS.Dev, SP 2010, Visual Studio — Tags: , , — Peter Holpar @ 22:44

Recently I made a minor modification to my local CKS.Dev version to enable accessing the Quick Deploy menu functionality through the keyboard shortcuts even when working on a code file. The standard behavior of version 2.1 is that you should select a SharePoint project in the Solution Explorer prior to using any of the key combination, otherwise you receive a message like this one:

The key combination (Alt + C, G) is bound to command (Copy to GAC/BIN) which is not currently available.

I found it to be a bit uncomfortable, so I loaded the solution into Visual Studio and made the required changes in the code. It turned out that altering the code was the easier part of the task.

When I tried to build the project, the following build errors were generated for the file CreatePkgDef:

Error    211    Could not load file or assembly ‘CKS.Dev, Version=2.1.0.0, Culture=neutral, PublicKeyToken=487fd6341a5c701f’ or one of its dependencies. Strong name validation failed. (Exception from HRESULT: 0x8013141A)
Error    212    Strong name validation failed. (Exception from HRESULT: 0x8013141A)

Fortunately, I found a good description of the problem and the solution in a blog post from Charlie Holland.

Based on the post (and the Readme.txt in the CKS Developer Edition\Signing folder) I’ve started an instance of Visual Studio Command Prompt (2010) and run the DisableStrongNameVerification.bat command. It runs the sn command that adds a verification entry for assembly ‘*,487fd6341a5c701f’ to disable strong name verification for the CKS.Dev assemblies.

I’ve uninstalled the original version of CKS.Dev and started debugging the modified one using the experimental instance of VS 2010.

I pressed Alt + C, G, the command was fired through the shortcut as expected, deployed my assembly to the GAC, but finally generated the following alert:

Command ID ‘Deployment.GetApplicationPoolName’ is invalid.

And the following trace message was displayed in the Output window:

Could not load file or assembly ‘CKS.Dev.Commands.Implementation.v4, Version=2.1.0.0, Culture=neutral, PublicKeyToken=487fd6341a5c701f’ or one of its dependencies. Strong name validation failed. (Exception from HRESULT: 0x8013141A)

So we have turned verification off, or might be not? What is the reason for that?

As you may know (if you don’t, you should read this post first), to bridge the gap between Visual Studio 2010 (x86 process) and SharePoint 2010 (x64 process), VS runs SharePoint commands through the x64 VSSPHOST4 process.

In the former steps we disabled strong name verification only for the x86 process (Visual Studio), but to disable it for x64 (VSSPHOST4) as well, we should start Visual Studio x64 Win64 Command Prompt (2010) and run the DisableStrongNameVerification.bat command again.

Note: You can check the independency of the different versions by starting a command prompt for both x86 and x64, and adding / removing / listing entries using the sn command and the –Vr / -Vx / -Vl switches.

Then kill the VSSPHOST4 process, or simply restart VS, and things work as we planned.

Blog at WordPress.com.