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.
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:
- // IECacheFilePattern property related members
- private const string IECacheFilePatternPropertyId = "IECacheFilePattern";
- private const string IECacheFilePatternPropertyDefaultValue = "";
- [DisplayName("IE cache file pattern")]
- [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")]
- [DefaultValue(IECacheFilePatternPropertyDefaultValue)]
- // we want our custom property to show up in the SharePoint section in the project properties property grid
- [Category("SharePoint")]
- public string IECacheFilePattern
- {
- get
- {
- string propertyValue;
- int hr = projectStorage.GetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
- (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
- // Try to get the current value from the project file; if it does not yet exist, return a default value.
- if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
- {
- propertyValue = IECacheFilePatternPropertyDefaultValue;
- }
- return propertyValue;
- }
- set
- {
- projectStorage.SetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
- (uint)_PersistStorageType.PST_PROJECT_FILE, value);
- }
- }
- // IECacheFileList property related members
- private const string IECacheFileListPropertyId = "IECacheFileList";
- private const string IECacheFileListPropertyDefaultValue = "";
- [DisplayName("IE cache file list")]
- [Description("This property specifies which files should be deleted from the IE cache when the Clear IE cache deployment step is activated")]
- [DefaultValue(RelatedTimerJobsPropertyDefaultValue)]
- // we want our custom property to show up in the SharePoint section in the project properties property grid
- [Category("SharePoint")]
- // use custom property editor to avoid "Constructor on type 'System.String' not found." error in design mode
- [Editor("System.Windows.Forms.Design.StringArrayEditor, System.Design", typeof(UITypeEditor))]
- [TypeConverter(typeof(CsvArrayConverter))]
- public String[] IECacheFileList
- {
- get
- {
- String propertyValue;
- int hr = projectStorage.GetPropertyValue(IECacheFileListPropertyId, string.Empty,
- (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
- // Try to get the current value from the project file; if it does not yet exist, return a default value.
- if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
- {
- propertyValue = IECacheFileListPropertyDefaultValue;
- }
- // remove accidental whitespaces
- String[] fileNames = propertyValue.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
- fileNames = Array.ConvertAll<String, String>(fileNames, fileName => fileName.Trim());
- return fileNames;
- }
- set
- {
- String propertyValue =
- (value == null) ?
- String.Empty :
- // remove accidental whitespaces
- String.Join("|", Array.ConvertAll<String, String>(value, fileName => fileName.Trim()));
- projectStorage.SetPropertyValue(IECacheFileListPropertyId, string.Empty,
- (uint)_PersistStorageType.PST_PROJECT_FILE, propertyValue);
- }
- }
- // IECacheDeleteRule property related members
- private const string IECacheDeleteRulePropertyId = "IECacheDeleteRule";
- private const IECacheDeleteRules IECacheDeleteRulePropertyDefaultValue = IECacheDeleteRules.OnlyCurrentSite;
- [DisplayName("IE cache delete rule")]
- [Description("This property specifies the relation between the current SharePoint site and files to be deleted from IE cache")]
- [DefaultValue(IECacheDeleteRulePropertyDefaultValue)]
- // we want our custom property to show up in the SharePoint section in the project properties property grid
- [Category("SharePoint")]
- [TypeConverter(typeof(IECacheDeleteRuleConverter))]
- public IECacheDeleteRules IECacheDeleteRule
- {
- get
- {
- // set default value
- IECacheDeleteRules propertyValue = IECacheDeleteRulePropertyDefaultValue;
- string propertyValueString;
- int hr = projectStorage.GetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
- (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValueString);
- // Try to get the current value from the project file; if it does not yet exist, return a default value.
- if (ErrorHandler.Succeeded(hr) && !String.IsNullOrEmpty(propertyValueString))
- {
- Enum.TryParse<IECacheDeleteRules>(propertyValueString, out propertyValue);
- }
- return propertyValue;
- }
- set
- {
- projectStorage.SetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
- (uint)_PersistStorageType.PST_PROJECT_FILE, value.ToString());
- }
- }
- public enum IECacheDeleteRules
- {
- [Description("Only from current site")]
- OnlyCurrentSite,
- [Description("All from current site")]
- AllFromCurrentSite,
- [Description("No site specific")]
- NoSiteSpecific
- }
- // based on EnumConverter example from
- // http://www.c-sharpcorner.com/uploadfile/witnes/using-propertygrid-in-net/
- class IECacheDeleteRuleConverter : EnumConverter
- {
- private Type enumType;
- public IECacheDeleteRuleConverter(Type type) : base(type)
- {
- enumType = type;
- }
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
- {
- return destType == typeof(string);
- }
- public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType)
- {
- FieldInfo fi = enumType.GetField(Enum.GetName(enumType, value));
- DescriptionAttribute dna = (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
- if (dna != null)
- return dna.Description;
- else
- return value.ToString();
- }
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type srcType)
- {
- return srcType == typeof(string);
- }
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- foreach (FieldInfo fi in enumType.GetFields())
- {
- DescriptionAttribute dna =
- (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
- if ((dna != null) && ((string)value == dna.Description))
- return Enum.Parse(enumType, fi.Name);
- }
- return Enum.Parse(enumType, (string)value);
- }
- }
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.
- internal static void ClearIECacheFile(ISharePointProject project)
- {
- try
- {
- LogToOutputWindows(project, "Clearing files from IE cache");
- SPVSProjectProps propertiesObject;
- if (project.Annotations.TryGetValue<SPVSProjectProps>(out propertiesObject))
- {
- String ieCacheFilePattern = propertiesObject.IECacheFilePattern;
- IEnumerable<String> ieCacheFileList = propertiesObject.IECacheFileList;
- SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule = propertiesObject.IECacheDeleteRule;
- List<String> filesDeleted = new List<String>();
- Func<Uri, bool> shouldDelete = new Func<Uri, bool>(u => ShouldDeleteIECacheFile(u,
- ieCacheFilePattern,
- ieCacheFileList,
- ieCacheDeleteRule,
- filesDeleted,
- project));
- DeleteIECache.ClearIEFiles(shouldDelete);
- LogToOutputWindows(project, String.Format("Number of files deleted from IE cache: {0}", filesDeleted.Count));
- }
- }
- catch (Exception ex)
- {
- LogToOutputWindows(project, String.Format("Clearing files from IE cache failed. Exception: {0}", ex.Message));
- }
- }
- internal static bool ShouldDeleteIECacheFile(Uri uri, String filePattern, IEnumerable<String> fileList,
- SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule, List<String> filesDeleted, ISharePointProject project)
- {
- bool result = false;
- Uri siteUrl = project.SiteUrl;
- Regex filePatternRegex = string.IsNullOrEmpty(filePattern) ? null : new Regex(filePattern);
- List<string> fileListEx = (fileList == null) ? new List<string>() : fileList.ToList();
- string fileName = uri.Segments[uri.Segments.Length - 1];
- bool isFromCurrentSite = uri.AbsoluteUri.IndexOf(siteUrl.AbsoluteUri, StringComparison.InvariantCultureIgnoreCase) == 0;
- if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.AllFromCurrentSite)
- {
- result = isFromCurrentSite;
- }
- else
- {
- result = ((fileListEx.Any(f => (f.ToUpper() == fileName.ToUpper()))) ||
- (filePatternRegex != null) && (filePatternRegex.IsMatch(fileName)));
- if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.OnlyCurrentSite)
- {
- result = result && isFromCurrentSite;
- }
- }
- if ((result) && (!filesDeleted.Contains(uri.AbsoluteUri)))
- {
- filesDeleted.Add(uri.AbsoluteUri);
- LogToOutputWindows(project, String.Format("Deleting file from IE cache: '{0}'", uri));
- }
- return result;
- }
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.
- [DeploymentStep("PHolpar.ClearIECache")]
- [Export(typeof(IDeploymentStep))]
- internal class ClearIECacheDeployStep : IDeploymentStep
- {
- public bool CanExecute(IDeploymentContext context)
- {
- return true;
- }
- public void Execute(IDeploymentContext context)
- {
- ISharePointProject project = context.Project;
- ExtensionHelper.ClearIECacheFile(project);
- }
- public void Initialize(IDeploymentStepInfo stepInfo)
- {
- stepInfo.Name = "Clear IE cache";
- stepInfo.Description = "This step deletes the specified files from the Internet Explorer cache of the current user.";
- stepInfo.StatusBarMessage = "IE cache is being cleared…";
- }
- }
To test the new extension, I’ve created a deployment configuration called Clear IE cache.
This DC has only a single deployment step, our new Clear IE cache DS.
Using the following project properties (reg. exp. used: ^.*\.(js|JS)$) we can delete all .js files that were cached from the active SharePoint site:
We can specify the files to be deleted explicitly in the IE cache file list property:
In this case these files would be deleted from the cache independently from the URL they were downloaded from:
The next screenshot displays a sample deployment for the previous configuration:
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.
