Making the sample XmlSiteMapProvider updatable with Cache dependencies.

One feature that isn't availible in the source for the XmlSiteMapProvider we released is the ability to update when the web.sitemap file has been updated.  The reason we couldn't provide this is that the mechanism we use inside XmlSiteMapProvider relies on some internal methods which map down to native calls.  Now, this can easily be done with cache dependencies but we chose not to modify the code that way so that users would see how the providers were actually coded and not some version designed to mimic the providers.

However, since this is clearly a nice feature and relatively easy to implement, we elected to blog on it after-the-fact.  Here you go, there's only a few line changes:

First, you need to hook up the handler.  It's actually only a few lines of code.  In the GetConfigDocument() function, there's a few lines like this:

if (!String.IsNullOrEmpty(_filename)) {
    //_handler = new FileChangeEventHandler(this.OnConfigFileChange);
    //HttpRuntime.FileChangesMonitor.StartMonitoringFile(_filename, _handler);
    ResourceKey = (new FileInfo(_filename)).Name;
}

The new version is: 

if (!String.IsNullOrEmpty(_filename)) {
    CacheItemRemovedCallback _handler = new CacheItemRemovedCallback(OnConfigFileChange);
    CacheDependency _dep = new CacheDependency(_filename);
    HttpContext.Current.Cache.Add(GetHashCode().ToString(), "", _dep,
        Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
        CacheItemPriority.Normal, _handler);
ResourceKey = (new FileInfo(_filename)).Name;
}

However, the entire function needs to be reorganized slightly to ensure this code always runs. The normal behavior is to cache the results an exit immediately, in the first version of this post, this behavior caused this code to only get executed the first time. Here's the new version:

private XmlDocument GetConfigDocument() {

if (_document == null)

{

if (!_initialized)

{

throw new InvalidOperationException(

SR.GetString(SR.XmlSiteMapProvider_Not_Initialized));

}

// Do the error checking here

if (_virtualPath == null)

{

throw new ArgumentException(

SR.GetString(SR.XmlSiteMapProvider_missing_siteMapFile, _siteMapFileAttribute));

}

if (!Path.GetExtension(_virtualPath).Equals(_xmlSiteMapFileExtension, StringComparison.OrdinalIgnoreCase))

{

throw new InvalidOperationException(

SR.GetString(SR.XmlSiteMapProvider_Invalid_Extension, _virtualPath));

}

// Ensure the appdomain virtualpath has proper trailing slash

_normalizedVirtualPath =

VirtualPathUtility.Combine(AppDomainAppVirtualPathWithTrailingSlash, _virtualPath);

// Make sure the file exists

CheckSiteMapFileExists();

_parentSiteMapFileCollection = new StringCollection();

XmlSiteMapProvider xmlParentProvider = ParentProvider as XmlSiteMapProvider;

if (xmlParentProvider != null && xmlParentProvider._parentSiteMapFileCollection != null)

{

if (xmlParentProvider._parentSiteMapFileCollection.Contains(_normalizedVirtualPath))

{

throw new InvalidOperationException(

SR.GetString(SR.XmlSiteMapProvider_FileName_already_in_use, _virtualPath));

}

// Copy the sitemapfiles in used from parent provider to current provider.

foreach (string filename in xmlParentProvider._parentSiteMapFileCollection)

{

_parentSiteMapFileCollection.Add(filename);

}

}

// Add current sitemap file to the collection

_parentSiteMapFileCollection.Add(_normalizedVirtualPath);

_filename = HostingEnvironment.MapPath(_normalizedVirtualPath);

_document = new ConfigXmlDocument();

}

if (!String.IsNullOrEmpty(_filename))

{

CacheItemRemovedCallback _handler = new CacheItemRemovedCallback(OnConfigFileChange);

CacheDependency _dep = new CacheDependency(_filename);

HttpContext.Current.Cache.Add(GetHashCode().ToString(), "", _dep,

Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,

CacheItemPriority.Normal, _handler);

ResourceKey = (new FileInfo(_filename)).Name;

}

return _document;

}

 

Next, look for the OnConfigFileChange() handler:

//private void OnConfigFileChange(Object sender, FileChangeEvent e)
//{
// // Notifiy the parent for the change.
// XmlSiteMapProvider parentProvider = ParentProvider as XmlSiteMapProvider;
// if (parentProvider != null)
// {
// parentProvider.OnConfigFileChange(sender, e);
// }
// Clear();
//}

And change it to this (only the function signature and the recursive call changes):

private void OnConfigFileChange(string key, object value, CacheItemRemovedReason reason)
{

    // Notifiy the parent for the change.
    UpdatableXmlSiteMapProvider parentProvider = ParentProvider as UpdatableXmlSiteMapProvider;
    if (parentProvider != null)
{
parentProvider.OnConfigFileChange(key, value, reason);
}
Clear();
}

And that's all there is to it.  The provider will now update with file changes (or any other time the key is forced out of cache).