| John's profileJohn West Blogs about Si...PhotosBlogLists | Help |
|
11/23/2008 Manipulating Sitecore LinksUpdate: most of this is covered by http://sdn.sitecore.net/Reference/Sitecore%206/Dynamic%20Links.aspx, which references http://trac.sitecore.net/LinkProvider. The release notes for Sitecore 6 081002 introduce a new web.config setting called Rendering.SiteResolving. If you use Redgate Reflector to analyze the getter for Sitecore.Configuration.Settings.Rendering.SiteResolving, you will see that it is called in two places: when using the RenderField pipeline, and when configuring options for an XSL rendering helper. It doesn't look like the Sitecore.Links.LinkManager.GetItemUrl() method respects this setting. You can work around this with code such as the following: Sitecore.Data.Items.Item item = Sitecore.Context.Item; Sitecore.Links.UrlOptions urlOptions = (Sitecore.Links.UrlOptions) Sitecore.Links.UrlOptions.DefaultOptions.Clone(); urlOptions.SiteResolving = Sitecore.Configuration.Settings.Rendering.SiteResolving; string url = Sitecore.Links.LinkManager.GetItemUrl(item,urlOptions); But that's a new method to remember to call. It would be preferable to override the link provider. Unfortunately, due to my lack of knowledge and time, or possibly due to the implementation, I couldn't figure out how to override the link provider perfectly. My link provider either enforces Rendering.SiteResolving all of the time, or never. I am not sure if this could cause any problems. For example, CMS users may prefer to authenticate against a single domain. If needed, the logic could set the Sitecore.Links.UrlOptions.SiteResolving property only if the Sitecore.Context.PageMode.IsPageEditor property is False. This feature alone would not require much work, so I decided to add some more features something else. There has been some discussion on the Sitecore Developer Network (SDN) forums about further optimizing Sitecore URLs for search engines, and it would be nice if Sitecore would use the alias for an item if one exists. I used the following properties, though you could use this for various URL processing requirements.
The logic for this is still relatively straightforward - override the GetItemUrl() and apply any differences. As with anything I post, this is largely untested, especially under various configurations. Configure the /configuraiton/sitecore/linkManager element of web.config as follows: <linkManager defaultProvider="sitecore">
<providers>
<clear />
<add name="sitecore" type="Namespace.Links.LinkProvider, assembly" addAspxExtension="true" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="always" languageLocation="filePath" useDisplayName="false">
<applySiteResolving>true</applySiteResolving>
<expandAliases>true</expandAliases>
<appendSlash>false</appendSlash>
</add>
</providers>
</linkManager>
The real logic is in the supporting Namespace.Web.Url class used to parse URLs. Sitecore provides facilities for building URLs and links, but I wanted somewhat more explicit control of URL components. I used the following properties:
The class includes a constructor and a method intended to work something like a factory. The ToString() method returns the URL. The ParseUrlString() method resets the object and parses a string to set properties. The Namespace.StringExtensions class extends the System.String class with the NthIndexOf() method. The Namespace.ItemExtensions class extends the Sitecore.Data.Items.Item class with the GetFriendliestUrl() method, which calls the appropriate method to determine the URL of an item. Then, to access the URL of an item: Sitecore.Data.Items.Item item = Sitecore.Context.Item; string url = item.GetFriendliestUrl(); You still need to override the link provider to ensure that Sitecore always applies your logic. Remember that unless you use IIS7 with an application pool in integrated mode, URLs should include the .aspx extension, so that ASP.NET provides Sitecore with an opportunity to process the request. Theoretically, you can work around this by mapping an empty extension to the ,NET ISAPI filter, but I don't think this is practical. Another workaround to this limitation of ASP.NET is to use an an ISAPI filter as described in this SDN scrapbook entry I wrote some time ago. The code is available at http://resources.thedotnetcms.com/sitecorejohn/linkextensions.zip. Update: The Content API Cookbook provides information about Sitecore dynamic link management. 11/20/2008 Reflecting on SitecoreI do not have access to the source code for Sitecore, so I often use Redgate's .NET Reflector to investigate the code. I think it's still fair to credit Lutz Roeder for this excellent tool. Redgate (which makes some other great products as well) can probably increase the value of Reflector more efficiently than an individual, such as by hosting online forums, managing newsletters, soliciting developers to build add-ins, and so forth. A recent Redgate .NET Reflector newsletter provided a link to an article by Jason Crease titled "First Steps with .NET Reflector", and another by Andrew Clarke titled "Using .NET Reflector Add-ins". I didn't even know about Reflector add-ins, so I hope to find (or possibly even develop) one that addresses some of my only complaints with this tool: that I can search by name for a class, struct, or enum, but I cannot search for a method by name, and I cannot search for a string within the disassembled code. I don't believe that using Reflector with Sitecore assemblies violates any Sitecore licensing terms, but if it does, I am sure that Sitecore will never enforce those clauses of the license. In order to prevent reverse-engineering the product, Sitecore obfuscates a small portion of the internal code that it doesn't expect most Sitecore developers to read. Most Sitecore developers primarily use APIs in the Sitecore.Kernel.dll assembly. Once in a while you might need to open Sitecore.Client.dll. Once you access a class, Reflector will prompt you to let it open additional assemblies as needed. The three Reflector commands I find most helpful are:
It's easy to determine how to investigate certain aspects of Sitecore. For instance, the properties of Sitecore.Context generally lead you to the most relevant classes, and you can investigate the classes referenced in web.config to see how Sitecore implements all sorts of features. When you're ready, you can inherit from or replace those classes with customizations. More advanced developers might be interested in how Sitecore implements certain features in the user interface. This usually comes down to one of two things: investigating a command in the Content Editor ribbon, or investigating an entire application such as the User Manager. To investigate ribbon commands, it helps to know a little more about how Sitecore works. Log in to the Sitecore Desktop as an administrator, click the database icon in the lower right corner, and then click core. You are now working with the Core database, which controls the Sitecore user interfaces, instead of the default Master database, which contains all versions of all content. Launch the Content Editor and navigate to the item you wish to investigate. Remember to switch back to the Master database when you're done investigating the Core database. For example, to investigate the Publish button in the Publish group on the Publish tab, in the Content Editor, navigate to /Sitecore/Content/Applications/Content Editor/Ribbons/Chunks/Publish/Publish (chunks being the original name for groups). In the Data section of this item, the Click field contains the token item:publishnow. This corresponds to an entry in the /App_Config/Commands.config file, which specifies the class that implements the logic that occurs when you click the Publish command, including the assembly that contains that class. The Menu field of this item references other items in the Core database that correspond to the commands on the menu that appears if you click the drop-down menu underneath the Publish command. Sitecore uses a really elegant (and hence, sometimes complex) approach for developing certain user interface components. Instead of hard-coding everything in .aspx files, Sitecore uses XML files under the /sitecore/shell/applications directory for a number of user interface components. The Sitecore SheerUI layer converts these XML files to ASP.NET controls, which render the user interface. Sitecore may not always name these files what you expect, so you might have to find a reference in the Core database or elsewhere to the item you need, or just look around and validate your guess before investigating too closely. There appear to be some XAML/Sheer tutorials under the /sitecore/shell/applications/xaml directory (my understanding is that Sitecore referred to SheerUI as XAML before Microsoft formalized XAML). These XML files reference classes that effectively provide code-beside for the UI components represented by the XML, and these classes tend to be in the Sitecore.Client.dll assembly. For example, the /sitecore/shell/applications/licenses/licensedetails/licensedetails.xml file implements the license details UI. The <CodeBeside> element in this file specifies the code-behind that implements the logic behind the license details UI component. One of the greatest things about Sitecore is that you can override almost any feature of the product with your own logic. You can change the classes referenced in web.config and /App_Config/Commands.config, you can add your own commands and other features to the ribbon and /App_Config/Commands.config, and you can override SheerUI components (copy the XML file from the source directory to the /sitecore/shell/override folder and change the <CodeBeside> attribute to reference your class and assembly). Note that Sitecore recently introduced a new technology called XAMLSharp, which has the effect described here. If you have read this entire post, you may also be interested in the MSDN Magazine .NET Tools article "Ten Must-Have Tools Every Developer Should Download Now", which I found in the Wikipedia page for Reflector. It's embarrassing to say I'm not as familiar with NUnit, NDoc, or NAnt as I could be. Update: The CodeSearch Reflector add-in seems to allow searching for strings in code. It's a little slow, which reminds me to write that I think it's a good idea to close assemblies that you're not using. My shortcut is to select one assembly, and then press CTRL-F4 until I have closed all of the assemblies. I couldn't find anything that would allow me to search for a method by name. Update Again: XSL developers can investigate signatures for XSL extension methods in the sc namespace by reviewing the Sitecore.Xml.Xsl.XslHelper class in the Sitecore.Kernel.dll assembly. Or review the Sitecore API documentation. Sitecore's XSL Presentation Component Reference provides an overview of common XSL extension methods, and lists the classes exposed by other namespaces in the section "Additional XSL Extension Method Classes". Update Once More: A coworker told me they use the FileDisassembler Reflector add-in to create source files from code. This coworker prefers to compile the code and use the debugger to see how it works instead of just reading it, but this could also be handy to override things that could otherwise be difficult to override (I think some cases that use this or contain private and protected methods). But be forewarned: using this approach to override components could prevent you from receiving bugfixes and could also present upgrade challenges. Yet Another Update (this time with Windows Live (Blog) Writer): Deblector sounds very useful for stepping through .NET framework assemblies, but for some reason I can’t seem to attach to w3wp.exe, and haven’t received a response to an email sent to the owner. 11/19/2008 Overriding Sitecore's Logic to Determine the Context LanguageThe Sitecore layout engine retrieves content from the Sitecore repository in the context language (Sitecore.Context.Language). The default logic to determine the context language is to use the first of these variables that specifies a value:
In the first two cases, Sitecore generates a session cookie so that subsequent requests do not have to include the URL query string or language prefix in the URL. If the user returns to the site without specifying the URL query string parameter or the language prefix in the URL path, their language selection would be lost. If the organization has not published a translation of the context item in the context language, Sitecore acts as if all fields in that item are empty. Some solutions need to augment the default logic for determining the context language. For instance, the developer may want to:
Additionally, ASP.NET uses the System.Threading.Thread.CurrentThread.CurrentUICulture and System.Threading.Thread.CurrentThread.CurrentCulture properties for localization, such as formatting dates. Sitecore does not set these properties based on the context language, so they default to the operating system configuration. It might be helpful if the language resolution logic set these properties. The following solution is relatively untested but could provide inspiration for a production-quality solution to meet these requirements. My excuses for not testing are that a developer should not test their own code and I am not even really a developer, but the issue is really that there are a lot of conditions to test, especially with different configurations of the dynamic link manager, and nobody is paying me to do this. Sitecore.Context.Language is a smart property, which means it follows the lazy load pattern: if code accesses this property when nothing has set it, the getter for the property contains logic to determine the context language. Sitecore uses the Sitecore.Pipelines.HttpRequest.LanguageResolver processor in the httpRequestBegin pipeline to determine the context language. Because Sitecore.Context.Language is a smart property, this processor probably isn't necessary, as some subsequent logic in the processing of the request is almost guaranteed to access Sitecore.Context.Language. In any case, we want to override this logic. Normally when adding logic to a pipeline, we replace an existing processor with our processor, or add our processor before or after a default processor. But in this case our processor depends on Sitecore.Context.Item, and therefore must appear after the Sitecore.Pipelines.HttpRequest.ItemResolver processor which sets the Sitecore.Context.Item property. Without investigating, we don't know whether processors between Sitecore.Pipelines.HttpRequest.LanguageResolver and Sitecore.Pipelines.HttpRequest.ItemResolver use Sitecore.Context.Language, so we'll just leave that default Sitecore.Pipelines.HttpRequest.LanguageResolver alone and add our processor after Sitecore.Pipelines.HttpRequest.ItemResolver. Compile the processor into your visual studio project. Add the custom processor after the default Sitecore.Pipelines.HttpRequest.ItemResolver processor in web.config: <processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel" /> <processor type="Namespace.Pipelines.HttpRequestBegin.LanguageResolver, assembly"> <persistLanguage>true</persistLanguage> <setCulture>true</setCulture> </processor> If you don't want to create a persistant cookie to persist the user's language preference between browser sessions, set the value of the <persistLanguage> element to false, or simply remove this property setter. To support fallback languages:
Note that asNeeded is the default value for the languageEmbedding attribute of the /configuration/sitecore/linkManager/providers/add element in web.config that controls the logic for generating friendly URLs. The logic applied for this value may not be exactly what you expect, which can result in multiple URLs for a single content item in a single language. I recommend setting this to always for solutions involving a single language, or never otherwise. My current code for this processor follows:
using System;
namespace Namespace.Pipelines.HttpRequestBegin
{
public class LanguageResolver
{
private int _fallbackDepthLimit = 5;
public int FallbackDepthLimit
{
set
{
_fallbackDepthLimit = value;
}
get
{
return _fallbackDepthLimit;
}
}
public bool SetCulture
{
get;
set;
}
public bool PersistLanguage
{
get;
set;
}
public void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
if (Sitecore.Context.Item == null)
{
string message = String.Format("{0} : context item null : {1}", this.GetType().ToString(),
Sitecore.Web.WebUtil.GetRawUrl());
Sitecore.Diagnostics.Log.Error( message, this);
}
else if ( Sitecore.Context.Site == null )
{
string message = String.Format("{0} : context site null : {1}", this.GetType().ToString(),
Sitecore.Web.WebUtil.GetRawUrl());
Sitecore.Diagnostics.Log.Error( message, this );
}
else if ( LanguageSetFromUrlPath()
|| LanguageSetFromQueryString()
|| LanguageSetFromCookie()
|| LanguageSetFromBrowserPreferences()
|| LanguageSetFromContextSite()
|| LanguageSetFromDefaultSetting()
|| LanguageSetToFirstExistingLanguage())
{
if (SetCulture)
{
System.Threading.Thread.CurrentThread.CurrentUICulture =
new System.Globalization.CultureInfo(Sitecore.Context.Language.Name);
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture(Sitecore.Context.Language.Name);
}
}
else
{
string message = String.Format("{0} : Unable to determine valid context language : {1}",
this.GetType().ToString(), Sitecore.Web.WebUtil.GetRawUrl());
Sitecore.Diagnostics.Log.Error(message, this );
}
}
private bool LanguageSetFromUrlPath()
{
return Sitecore.Context.Data.FilePathLanguage != null && SetLanguage(Sitecore.Context.Data.FilePathLanguage.Name, false, true, 0);
}
private bool LanguageSetFromQueryString()
{
return SetLanguage(System.Web.HttpContext.Current.Request.QueryString["sc_lang"], true, true, 0);
}
private bool LanguageSetFromCookie()
{
return SetLanguage(Sitecore.Web.WebUtil.GetCookieValue(Sitecore.Context.Site.GetCookieKey("lang")), false, true, 0);
}
private bool LanguageSetFromBrowserPreferences()
{
string langs = System.Web.HttpContext.Current.Request.ServerVariables["HTTP_ACCEPT_LANGUAGE"];
if (!String.IsNullOrEmpty(langs))
{
foreach (string lang in Sitecore.StringUtil.Split(langs, ',', true))
{
string langName = lang;
if (lang.IndexOf(';') > -1)
{
langName = lang.Substring(0, lang.IndexOf(';'));
}
if (SetLanguage(langName, false,false, 0))
{
return true;
}
}
}
return false;
}
private bool LanguageSetFromContextSite()
{
return SetLanguage(Sitecore.Context.Site.Language, false, true, 0);
}
private bool LanguageSetFromDefaultSetting()
{
return SetLanguage(Sitecore.Configuration.Settings.DefaultLanguage, false,true, 0);
}
private bool LanguageSetToFirstExistingLanguage()
{
foreach (Sitecore.Globalization.Language language in Sitecore.Context.Item.Database.Languages)
{
if (SetLanguage(language.Name, false, false, 0))
{
return true;
}
}
return false;
}
private bool SetLanguage(string languageName, bool spanRequests, bool fallback, int fallbackDepth )
{
if (!String.IsNullOrEmpty(languageName))
{
foreach (Sitecore.Globalization.Language compare in Sitecore.Context.Item.Database.Languages)
{
if (languageName.Equals(compare.Name, StringComparison.InvariantCultureIgnoreCase))
{
if (HasVersionInLanguage(Sitecore.Context.Item, compare))
{
SetContextLanguage(compare, spanRequests);
return true;
}
else if (fallback)
{
Sitecore.Data.Items.Item languageItem =
Sitecore.Context.Item.Database.GetItem("/sitecore/system/languages" + compare.Name);
if (languageItem != null)
{
string fallbackLanguageName = languageItem["fallback language"];
if (!String.IsNullOrEmpty(fallbackLanguageName))
{
if (fallbackLanguageName == languageName)
{
string message = String.Format("{0} : invalid fallback language {1} in {2}",
this.GetType().ToString(), fallbackLanguageName,
languageItem.Paths.FullPath);
Sitecore.Diagnostics.Log.Error( message, this );
}
else
{
if ( fallbackDepth < FallbackDepthLimit )
{
return( SetLanguage(fallbackLanguageName, spanRequests, fallback, fallbackDepth++));
}
else
{
string message = String.Format("{0} : Fallback depth limit {1} exceeded processing {2}",
this.GetType().ToString(), FallbackDepthLimit, fallbackLanguageName);
Sitecore.Diagnostics.Log.Warn(message, this);
}
}
}
}
}
}
}
}
return false;
}
private bool HasVersionInLanguage(Sitecore.Data.Items.Item item,Sitecore.Globalization.Language language)
{
Sitecore.Data.Items.Item langItem = Sitecore.Context.Item.Database.GetItem(Sitecore.Context.Item.ID, language);
return langItem.Versions.Count > 0;
}
private void SetContextLanguage(Sitecore.Globalization.Language language, bool spanRequests )
{
Sitecore.Context.SetLanguage(language, spanRequests);
if ( spanRequests && PersistLanguage )
{
string cookieName = Sitecore.Context.Site.GetCookieKey("lang");
Sitecore.Web.WebUtil.SetCookieValue( cookieName, language.Name,DateTime.MaxValue);
}
}
}
}
This code could definitely use some refactoring. The following properties, which default to false unless set in the processor signature in web.config, serve the purposes described:
The following methods serve the purposes described:
One thing that concerns me is how ASP.NET uses the language associated with the current thread. If we assume that the entire system processes the entire request using this thread, or passes this setting to any child threads, then I think this should be reliable. But this is obviously far beyond my knowledge of ASP.NET threading. For me, it seemed to work using the Sitecore.Web.UI.WebControls.FieldRenderer Web control, the <sc:date> XSL extension control, and a custom XSL extension method, but my test system doesn't have the kind of load that might raise threading issues. Updated: New code. 11/14/2008 MSDN Magazine Utility Spotlight Article: 12 Steps To Faster Web Pages With Visual Round Trip AnalyzerThe November 2008 issue of MSDN Magazine has a Utility Spotlight article by Jim Pierson titled "12 Steps To Faster Web Pages With Visual Round Trip Analyzer" (http://msdn.microsoft.com/en-us/magazine/dd188562.aspx). Much of this content is relatively technical, including the explanation of Visual Round Trip Analyzer. But there are also some tips that should be relatively easy to implement for any Sitecore solution. I won't try to summarize the article, but point out and expand on what I expect would be the easiest suggestions to try.
11/10/2008 Southern Methodist University Mailing List for Higher Education and Public Sector Sitecore AdministratorsSitecore USA customer Southern Methodist University has created a mailing list for administrators using or considering Sitecore for their Web solutions. To join the list, see http://sdn.sitecore.net/forum/ShowPost.aspx?PostID=12546#12546. The list owner specifically requests no posting of advertisements on the list. |
|
|