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

1 Comment »

  1. Awesome and making it a deployment step: Perfect.

    Would be great to have it integrated into CKS.DEV though to not have to install yet another Extension.

    Comment by m00ntear — March 6, 2013 @ 13:03


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: