How to make the cache to refresh when the XML is changed? - mvcsitemapprovider

I am using MvcSiteMapProvider 4.6.3, MVC 4. Using DI to config the Sitemap.
this.For<System.Runtime.Caching.ObjectCache>()
.Use(s => System.Runtime.Caching.MemoryCache.Default);
this.For(typeof (ICacheProvider<>)).Use(typeof (RuntimeCacheProvider<>));
var rootCacheDependency = this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
.Ctor<string>("fileName").Is(rootFileName);
var rootCacheDetails = this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(rootCacheDependency);
var cacheDetails = new List<SmartInstance<CacheDetails>>();
var xmlSources = new List<SmartInstance<FileXmlSource>>();
How to make it automatically update the cache when the Sitemap xml is updated?
I am upgrading MvcSitemapProvider from v3 to v4.
In version 3, it seems the sitemap is automatically refreshed.
I did set the cache expiration time to be 5 min, is this causing problem?
TimeSpan absoluteCacheExpiration = TimeSpan.FromMinutes(5);
var rootCacheDetails = this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(rootCacheDependency);
UPDATE
When I change the sitemap xml file the cache is not updated till 5 min the cache expire.
I am using multiple sitemap xml files.
var sitmapPath = HostingEnvironment.MapPath("~/Sitemaps");
var sitemaps = new List<string>();
if (sitmapPath != null)
{
sitemaps.AddRange(Directory.GetFiles(sitmapPath, "*.sitemap"));
}
foreach (var sitemapFileName in sitemaps)
{
var cacheDependencie =
this.For<ICacheDependency>()
.Use<RuntimeFileCacheDependency>()
.Ctor<string>("fileName")
.Is(sitemapFileName);
cacheDetails.Add(this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(cacheDependencie));
xmlSources.Add(this.For<IXmlSource>().Use<FileXmlSource>()
.Ctor<string>("fileName").Is(sitemapFileName));
}
Will this be the reason it's not working?

I don't see a problem with the code you posted. However, it is the RuntimeFileCacheDependency that will make it reload when the XML is changed.
The RuntimeFileCacheDependency expects the fileName argument to be an absolute path. So you must convert it using HostingEnvironment.MapPath before providing it to the RuntimeFileCacheDependency constructor.
var rootFileName = HostingEnvironment.MapPath("~/root.sitemap");
Response to Your Update
The purpose of the cacheDetails object is to specify the caching policy for a single SiteMapBuilderSet instance. If you look further down in the (original) DI module, notice that the variable is passed to the constructor of this class.
// Configure the builder sets
this.For<ISiteMapBuilderSetStrategy>().Use<SiteMapBuilderSetStrategy>()
.EnumerableOf<ISiteMapBuilderSet>().Contains(x =>
{
x.Type<SiteMapBuilderSet>()
.Ctor<string>("instanceName").Is("default")
.Ctor<bool>("securityTrimmingEnabled").Is(securityTrimmingEnabled)
.Ctor<bool>("enableLocalization").Is(enableLocalization)
.Ctor<bool>("visibilityAffectsDescendants").Is(visibilityAffectsDescendants)
.Ctor<bool>("useTitleIfDescriptionNotProvided").Is(useTitleIfDescriptionNotProvided)
.Ctor<ISiteMapBuilder>().Is(builder)
.Ctor<ICacheDetails>().Is(cacheDetails); // <- caching specified here explicitly.
});
This is what is used to expire the cache, but it is a completely separate mechanism from the part that specifies to use multiple files to build a SiteMap:
// Register the sitemap node providers
var siteMapNodeProvider = this.For<ISiteMapNodeProvider>().Use<CompositeSiteMapNodeProvider>()
.EnumerableOf<ISiteMapNodeProvider>().Contains(x =>
{
x.Type<XmlSiteMapNodeProvider>()
.Ctor<bool>("includeRootNode").Is(true)
.Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
.Ctor<IXmlSource>().Is(rootXmlSource);
// NOTE: Each additional XmlSiteMapNodeProvider instance for the same SiteMap instance must
// specify includeRootNode as "false"
x.Type<XmlSiteMapNodeProvider>()
.Ctor<bool>("includeRootNode").Is(false)
.Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
.Ctor<IXmlSource>().Is(childXmlSource1);
x.Type<XmlSiteMapNodeProvider>()
.Ctor<bool>("includeRootNode").Is(false)
.Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
.Ctor<IXmlSource>().Is(childXmlSource2);
// Add additional XmlSiteMapNodeProviders here (with includeRootNode as "false")...
// You only need this if you intend to use MvcSiteMapNodeAttribute in your application
x.Type<ReflectionSiteMapNodeProvider>()
.Ctor<IEnumerable<string>>("includeAssemblies").Is(includeAssembliesForScan)
.Ctor<IEnumerable<string>>("excludeAssemblies").Is(new string[0]);
});
// Register the sitemap builders
var builder = this.For<ISiteMapBuilder>().Use<SiteMapBuilder>()
.Ctor<ISiteMapNodeProvider>().Is(siteMapNodeProvider);
This is how to specify multiple XML files for a single SiteMap, but it is also possible to make each XML file into its own SiteMap instance by passing each instance of XmlSiteMapNodeProvider to a separate SiteMapBuilder and a separate SiteMapBuilderSet as described in Multiple SiteMaps in One Application.
IMPORTANT: For multiple XML files to work on a single SiteMap instance, you must specify the same key for the root node of each SiteMap as shown at the bottom of this answer. But you cannot specify a node representing the same controller action in more than one XML file (other than the root node).
If you need more flexibility than this, I would suggest implementing your own XmlSiteMapNodeProvider or abandoning the idea of using XML altogether, since using ISiteMapNodeProvider or IDynamicNodeProvider is much more flexible.
Now, back to the caching. If you are indeed using multiple XML files in the same SiteMap instance, you need to use a RuntimeCompositeCacheDependency so each of the files will be considered a dependency for the same cache, but you must use a single instance of CacheDetails.
var rootCacheDependency =
this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
.Ctor<string>("fileName").Is(rootAbsoluteFileName);
var childCacheDependency1 =
this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
.Ctor<string>("fileName").Is(childAbsoluteFileName1);
var childCacheDependency2 =
this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
.Ctor<string>("fileName").Is(childAbsoluteFileName2);
var cacheDependency =
this.For<ICacheDependency>().Use<RuntimeCompositeCacheDependency>()
.Ctor<ICacheDependency[]>().Is(new ICacheDependency[]
{
(ICacheDependency)rootCacheDependency,
(ICacheDependency)childCacheDependency1,
(ICacheDependency)childCacheDependency2
});
var cacheDetails =
this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(cacheDependency);

Related

Best approach for loading Kentico 10 web nodes including caching and permission checking

I have a custom product type that gets displayed in a custom listing web part. I was trying to cache the items for performance reasons, but it's also important to check user permissions as not all products are visible to all users.
private static IList<TreeNode> GetUniqueProducts(string clientId, string path)
{
var pages = CacheHelper.Cache(cs => GetProducts(cs, path), new CacheSettings(10, "cus|" + clientId));
return GetUniqueProductNamesItems(pages);
}
private static IList<TreeNode> GetProducts(CacheSettings cacheSettings, string rootPath)
{
var pages = DocumentHelper.GetDocuments().Types("CUS.Product")
.Path(rootPath, PathTypeEnum.Children)
.Published().CheckPermissions().ToList();
if (cacheSettings.Cached)
{
cacheSettings.CacheDependency = CacheHelper.GetCacheDependency("nodes|custom|cus.product|all");
}
return pages;
}
However I realise that this is caching the first user's list of products. When in fact I want to store the full list of products in cache - but then check permissions before they get displayed.
The only way of checking permission seems to be as part of a DocumentQuery as per above - but I don't know how to apply that to a cached list of products - or on an individual node.
So is there a good way to achieve what I want? Without having to loop through each node and individually check user is authorised to access the node ?
You are missing the caching part in your code and I am not quire sure about your cache dependencies.
var pages = CacheHelper.Cache(cs =>
{
var result = CMS.DocumentEngine.DocumentHelper.GetDocuments().Types("CUS.Product").Path(rooPath, CMS.DocumentEngine.PathTypeEnum.Children).Published().ToList();
if (cs.Cached) { cs.CacheDependency = CacheHelper.GetCacheDependency("cus.product|all"); }
return result;
},
new CacheSettings(CacheHelper.CacheMinutes(CurrentSite.SiteName), "custom_products"));
If you checking the user read permissions it means you kinda caching per user. Then your cache should be done per user i.e. cachename should be "custom_products"+ UserID.ToString() or something like this.

Making jslink target specific list

Background
I got a page where I’m showing two list views from two separate lists which both have Custom List as their ListTemplate. They got their separate jslink file cause I don’t want them to look alike.
Problem
The js link file targets both listviews since they use the same Template.
Code
(function () {
var listContext = {};
listContext.Templates = {};
listContext.ListTemplateType = 100;
listContext.Templates.Header = "<div><ul>";
listContext.Templates.Footer = "</ul></div>";
listContext.Templates.Item = LinkTemplate;
SPClientTemplates.TemplateManager.RegisterTemplateOverrides(listContext);
})();
Question
Is there any way to make the js only target a specific list?
Ended up going with Paul Hunts solution that he writes about on myfatblog.co.uk. http://www.myfatblog.co.uk/index.php/2013/09/listview-web-part-issues-with-jslink-and-display-templates-a-solution/
The script ended up looking like this and I pasted it into the jslink function where I define what listContext to override.
// Override the RenderListView once the ClientTemplates.JS has been called
ExecuteOrDelayUntilScriptLoaded(function(){
// Copy and override the existing RenderListView
var oldRenderListView = RenderListView;
RenderListView = function(ctx,webPartID)
{
// Check the title and set the BaseViewId
if (ctx.ListTitle == "List")
ctx.BaseViewID = "list";
//now call the original RenderListView
oldRenderListView(ctx,webPartID);
}
},"ClientTemplates.js");

Check URL exists in ApplicationContentUriRules

I have an app that is loading URLs into a WebView (x-ms-webview). When the user makes the request, I would like to compare the URL they are trying to load with the "whitelisted" URLs in the ApplicationContentUriRules and warn them if it is not there. Any ideas how I might accomplish this?
There isn't a direct API for pulling information out of the manifest, but there are options.
First, you can just maintain an array of those same URIs in your code, because to change them you'd have to change the manifest and update your package anyway, so you would update the array to match. This would make it easy to check, but increase code maintenance.
Such an array would also let you create a UI in which the user can enter only URIs that will work, e.g. you can offer possibilities from a drop-down list instead of letting the user enter anything.
Second, you can read the manifest XML into a document directly and parse through it to get to the rules. Here's some code that will do that:
var uri = new Windows.Foundation.Uri("ms-appx:///appxmanifest.xml");
var doc;
Windows.Storage.StorageFile.getFileFromApplicationUriAsync(uri).then(function (file) {
return Windows.Storage.FileIO.readTextAsync(file);
}).done(function (text) {
doc = new Windows.Data.Xml.Dom.XmlDocument();
doc.loadXml(text);
var acur = doc.getElementsByTagName("ApplicationContentUriRules");
if (acur !== null) {
var rules = acur[0].getElementsByTagName("Rule");
for (var i = 0; i < rules.length; i++) {
console.log(rules[i].getAttribute("Match") + " - " + rules[i].getAttribute("Type"));
}
}
});
You could probably just get "Rule" tags directly from the root doc, because I don't think anything else in the manifest uses that kind of node, but to future-proof it's better to get the ApplicationContentUriRules first. As you can see, the "Match" attribute is what holds the URI for that rule, but you also need to make sure that "Type" is "include" and not "exclude".

Defalut XmlSiteMapProvider implementation cannot use SiteMap.FindSiteMapNode?

I just upgrade MvcSiteMapProvider from v3 to v4.6.3.
I see the upgrade note indicate:
In general, any reference to System.Web.SiteMap.Provider will need to be updated to MvcSiteMapProvider.SiteMaps.Current
I am trying to get the sitemap node by using:
SiteMaps.Current.FindSiteMapNode(rawUrl)
But it always return null
I looked into the code. In the sitemap it's actually calling the function:
protected virtual ISiteMapNode FindSiteMapNodeFromUrlMatch(IUrlKey urlToMatch)
{
if (this.urlTable.ContainsKey(urlToMatch))
{
return this.urlTable[urlToMatch];
}
return null;
}
It's trying to find a match in the urlTable.
I am using Default implementation of XmlSiteMapProvider .
It define var url = node.GetAttributeValue("url");
siteMapNode.Url = url;
siteMapNode.UrlResolver = node.GetAttributeValue("urlResolver");
So if I did not define url or urlResolver attribute in the .sitemap file. These variables a set to empty string, when generate the node.
And when this nodes are passed to AddNode function in SiteMap.
When adding the node
bool isMvcUrl = string.IsNullOrEmpty(node.UnresolvedUrl) && this.UsesDefaultUrlResolver(node);
this code will check if there is url or urlResolver
// Only store URLs if they are clickable and are configured using the Url
// property or provided by a custom URL resolver.
if (!isMvcUrl && node.Clickable)
{
url = this.siteMapChildStateFactory.CreateUrlKey(node);
// Check for duplicates (including matching or empty host names).
if (this.urlTable
.Where(k => string.Equals(k.Key.RootRelativeUrl, url.RootRelativeUrl, StringComparison.OrdinalIgnoreCase))
.Where(k => string.IsNullOrEmpty(k.Key.HostName) || string.IsNullOrEmpty(url.HostName) || string.Equals(k.Key.HostName, url.HostName, StringComparison.OrdinalIgnoreCase))
.Count() > 0)
{
var absoluteUrl = this.urlPath.ResolveUrl(node.UnresolvedUrl, string.IsNullOrEmpty(node.Protocol) ? Uri.UriSchemeHttp : node.Protocol, node.HostName);
throw new InvalidOperationException(string.Format(Resources.Messages.MultipleNodesWithIdenticalUrl, absoluteUrl));
}
}
// Add the URL
if (url != null)
{
this.urlTable[url] = node;
}
Finally no url is add to the urlTable, which result in FindSiteMapNode cannot find anything.
I am not sure if there needs to be specific configuration. Or should I implement custom XmlSiteMapProvider just add the url.
ISiteMapNodeProvider instances cannot use the FindSiteMapNode function for 2 reasons. The first you have already discovered is that finding by URL can only be done if you set the url attribute explicitly in the node configuration. The second reason is that the SiteMapBuilder doesn't add any of the nodes to the SiteMap until all of the ISiteMapNodeProvider instances have completed running, so it would be moot to add the URL to the URL table anyway.
It might help if you explain what you are trying to accomplish.
The ISiteMapNodeProvider classes have complete control over the data that is added to the SiteMapNode instances and they also have access to their parent SiteMapNode instance. This is generally all that is needed in order to populate the data. Looking up another SiteMapNode from the SiteMap object while populating the data is not supported. But as long as the node you are interested in is populated in the same ISiteMapNodeProvider instance, you can just get a reference to it later by storing it in a variable.
Update
Okay, I reread your question and your comment and it now just seems like you are looking in the wrong place. MvcSiteMapProvider v4 is no longer based on Microsoft's SiteMap provider model, so using XmlSiteMapProvider doesn't make sense, as it would sidestep the entire implementation. The only case where this might make sense is if you have a hybrid ASP.NET and ASP.NET MVC application that you want to keep a consitant menu structure between. See Upgrading from v3 to v4.
There are 2 stages to working with the data. The first stage (the ISiteMapBuilder and ISiteMapNodeProvider) loads the data from various sources (XML, .NET attributes, DynamicNodeProviders, and custom implementations of ISiteMapNodeProvider) and adds it to an object graph that starts at the SiteMap object. Much like Microsoft's model, this data is stored in a shared cache and only loaded when the cache expires. This is the stage you have been focusing on and it definitely doesn't make sense to lookup nodes here.
The second stage is when an individual request is made to access the data. This is where looking up data based on a URL might make sense, but there is already a built-in CurrentNode property that finds the node matching the current URL (or more likely the current route since we are dealing with MVC) which in most cases is the best approach to finding a node. Each node has a ParentNode and ChildNodes properties that can be used to walk up or down the tree from there.
In this second stage, you can access the SiteMap data at any point after the Application_Start event such as within a controller action, in one of the built in HTML helpers, an HTML helper template in the /Views/Shared/DisplayTemplates/ directory, or a custom HTML helper. This is the point in the application life cycle which you might call the lines SiteMaps.Current.FindSiteMapNode(rawUrl) or (more likely) SiteMaps.Current.CurrentNode to get an instance of the node so you can inspect its Attributes property (the custom attributes).
public ActionResult About()
{
ViewBag.Message = "Your app description page.";
var currentNode = MvcSiteMapProvider.SiteMaps.Current.CurrentNode;
string permission = currentNode.Attributes.ContainsKey("permission") ? currentNode.Attributes["permission"].ToString() : string.Empty;
string programs = currentNode.Attributes.ContainsKey("programs") ? currentNode.Attributes["programs"].ToString() : string.Empty;
string agencies = currentNode.Attributes.ContainsKey("agencies") ? currentNode.Attributes["agencies"].ToString() : string.Empty;
// Do something with the custom attributes of the About page here
return View();
}
The most common usage of custom attributes is to use them from within a custom HTML helper template. Here is a custom version of the /Views/Shared/DisplayTemplates/SiteMapNodeModel.cshtml template that displays the custom attributes. Note that this template is called recursively by the Menu, SiteMapPath, and SiteMap HTML helpers. Have a look at this answer for more help if HTML helper customization is what you intend to do.
#model MvcSiteMapProvider.Web.Html.Models.SiteMapNodeModel
#using System.Web.Mvc.Html
#using MvcSiteMapProvider.Web.Html.Models
#if (Model.IsCurrentNode && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper") {
<text>#Model.Title</text>
} else if (Model.IsClickable) {
if (string.IsNullOrEmpty(Model.Description))
{
#Model.Title
}
else
{
#Model.Title
}
} else {
<text>#Model.Title</text>
}
#string permission = Model.Attributes.ContainsKey("permission") ? Model.Attributes["permission"].ToString() : string.Empty
#string programs = Model.Attributes.ContainsKey("programs") ? Model.Attributes["programs"].ToString() : string.Empty
#string agencies = Model.Attributes.ContainsKey("agencies") ? Model.Attributes["agencies"].ToString() : string.Empty
<div>#permission</div>
<div>#programs</div>
<div>#agencies</div>

Saving per-user or per-document preferences in a Photoshop script

I'm working on a Photoshop script in JavaScript using ExtendScript. My script allows some user input, and I'd like to save it between uses. That is, I'm looking for a way to save a simple string or numeric value under a particular key so that I'll be able to access it on subsequent uses of the script. Simply put, I want to save a preference for my script. How do I do that?
Even better would be to be able to save at least some preferences on a per-document basis. Is that possible? That is, can I store an arbitrary bit of data with a document?
You can use put/get custom options to save preference parameters that persist across Photoshop launches:
const kMyFlag = app.stringIDToTypeID( "myFlag" );
const kMyNumber = app.stringIDToTypeID( "myNumber" );
const kMySettings = "mySettings";
function saveSettings()
{
var desc = new ActionDescriptor();
desc.putBoolean(kMyFlag, true);
desc.putInteger(kMyNumber, 42);
// "true" means setting persists across Photoshop launches.
app.putCustomOptions( kMySettings, desc, true );
}
function getSettings()
{
var desc = app.getCustomOptions( kMySettings );
return [desc.getBoolean( kMyFlag ), desc.getInteger( kMyNumber )];
}
You have some options. You can create a text file and write to it using the File object:
var prefs = new File("~/desktop/prefs.txt");
prefs.open("w"); // or "a" to append
prefs.writeln("user:lenny;favorite_color:ff6600;likes:sunsets;");
...if you wanted your preferences tied to the script itself.
If you want per-document preferences you could write a string to one of the metadata fields of the file your working on using Document.info like this (using the 'instructions' field but you could use any writable field):
var doc = app.activeDocument;
doc.info.instructions = "user:lenny;favorite_color:ff6600;likes:sunsets;";
//alert(doc.info.instructions); // see, it works!
As for how to actually format the string you could just do it like a simple config file or, if you have a complex user preferences object you could use the XML object to construct and serialize it. JSON would be great for this but there is no JSON object in Extendscript, unfortunately.
For per-document prefs I suggest the use of the XMP Metadata. You can find example snippet here: http://forums.adobe.com/thread/790973. You can leverage AdobeXMPScript library to create your own namespace like it is suggested in the link by Paul Riggott.

Resources