Quantcast
Channel: Project Lifecycle
Viewing all 178 articles
Browse latest View live

Include File References Patch Issue

$
0
0

*Update 2

I also found that there was an issue with the <using> statements as well that required an attribute to be added. When I opened the Configure-Assign (Insert Options) or tried to add from template, I would get the following exception:

The type or namespace name 'EventArgs' could not be found.

Once I added an arbitrary attribute to the <using> tag it was fine.

*Update 1

So Sitecore Support got back to me pretty quickly and told me that if I just add an attribute that makes the reference unique that's how to solve the issue. Here's the relevant code:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<ui>
			<references>
				<reference comment="MyLibrary">/bin/MyLibrary.dll</reference>
			</references>
		</ui>
	</sitecore>
</configuration>

The system I manage was originally built on Sitecore 5.3 and not all the latest and greatest methodologies are being used so when I have time I try to update the system. This time, while working to migrate all the customizations to the web.config to an include file I ran into a slight snag. My longtime friend and confidant, the ever charming System Exception or collegially known as the Yellow Screen of Death:

The type 'System.ComponentModel.IComponent' is defined in an assembly that is not referenced. You must add a reference to assembly 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

Usually they're not too difficult to root out but this one really confused me. I mean who doesn't have the 'System' library referenced. I'm pretty sure it's included by default. Anyway after finding nothing useful on google I was left to my only option. Go back to the original web.config and move the changes over line by line until I hit the issue.

Sure enough the issue resided with the <references> section. Now looking back I guess I should've guessed this would be the problem but hindsite, well, it just is what it is. I checked the rendered config on the show config page [/sitecore/admin/showconfig.aspx] and found that my inserted reference was showing up before the System.dll reference. Ah, that's the issue. It's not being included in the right order. Okay so I just need to put it last and I'll be fine.

Now typically when you patch config something you're telling it to insert before or after something but in this case there are no attributes to use to tell it where to insert. I know I read that I could tell it what position to insert into by giving it the position. It's not optimal but let's just see. To my chagrin, this didn't work. I checked the rendered config on the show config page again and it was showing up correctly but it was still throwing the error. Ok, so let's try and add an attribute like I've seen on John West's blog and insert it after that item using the attribute query. It was rendered right but still throwing the error.

With my options running out my only remaining choice was to just put it back into the web.config. Not what I wanted to do but it did work. For the time being I'll be adding a ticket to the support site to make sure Sitecore knows about it. Who knows maybe it's already a known issue.


Site Specific Insert Option Rules For Sitecore

$
0
0

I recently had the need to add a new template type for a single site on my multi-site system and wanted to be able to provide insert options for this template on content folders only in this site. The content folders are a global template and are shared across multiple sites so adding this new template type to the insert options of the content folder was not a viable option.

I know, from previous experience that rules engine would be great solution for this but there were a few additional constraints that I had which the default settings didn't provide for. I wanted to limit the scope of the rule to work only on items in that site. Using the default setup I would have to specify that the selected item would need to be an ancestor of the web root item but this isn't a preferable option since I would need to add this condition to every rule.

I wanted to store the rules at least in folders separated by site and I figured I could use this separation to know when to run the rules and save the added conditions. By default the rules for all insert options are stored in a single folder within Sitecore in /sitecore/system/rules/insert options/rules, which means that rules for all sites is in one folder. This isn't optimal because the list could potentially become very long and difficult to know which rules are for which sites. Also I wanted to store the rules for each individual site separately so that when a site is taken down and removed from the system all the rules would be easy to remove along with the site specific layouts, sublayouts and templates. I potentially could add subfolders to the insert rules. I would have to override the default processor so that it would look recursively into folders but I would also have to rely on finding the folder to match the site name which could be flaky since someone could misspell the folder. I decided to store the rules for the site below the site root at the same level at the home node for the site.

site specific rules folder

Now that I had decided where to store the rule I had to figure out the best way to apply them. I could either add the rules to the template or override the processor. Adding the rules to the template meant that it would be cluttered with rules from all sites and would require maintenance if/when sites were added or removed. I decided that the best option was to override the processor. This would allow me to query for the rules from the site start path by matching it to the current item and then add any insert options that are stored there.

I found that the pipeline processor uiGetMasters/RunRules is what needs to be overridden. I pulled the source from a decompiler and noticed that it was directly looking for the global folder. I dug a little deeper and found that its taking all the children of the folder and passing it to an overload of the GetRules methods that takes an IEnumerable<Item>. From here I knew that I still wanted to include the global items, I just wanted to add a few more things to the enumeration. I used the context item that is passed in with the mastersArgs and checked the ancestors for a site root node. If one is found then I look through its children for a rules folder and then check the rules folder for any insert option rules in its ancestors. I then add these to the list and then run rules like it was originally.

Here's the code for my override of the RunRules class: 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Pipelines.GetMasters;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;
using Sitecore.Rules.InsertOptions;
using Sitecore.Rules;

namespace MySite.Pipelines.GetMasters
{
	public class RunRules
	{
		protected string RulesFolder = "RulesFolder";
		protected string InsertOptionsRule = "{664E5035-EB8C-4BA1-9731-A098FCC9127A}";
		protected string Website = "WebsiteRoot";

		// Methods
		public void Process(GetMastersArgs args) {
			
			//check if null
			Assert.ArgumentNotNull(args, "args");
			if (args.Item == null)
				return;

			//init start list
			List<Item> insertRuleItems = new List<Item>();
			
			//skip permissions
			using (new SecurityDisabler()) {

				//get the default rules
				Item DefaultRules = args.Item.Database.GetItem("/sitecore/system/Settings/Rules/Insert Options/Rules");
				if (DefaultRules != null)
					insertRuleItems.AddRange(DefaultRules.Children);

				//get any site specific rules.
				Item SiteRoot = GetSiteItem(args.Item);
				if (SiteRoot != null) {
					Item RulesFolder = ChildByTemplate(SiteRoot, this.RulesFolder);
					if(RulesFolder != null) {
						var rules = from val in RulesFolder.Axes.GetDescendants()
									where IsTemplateID(val, this.InsertOptionsRule)
									select val;
						if (rules.Any())
							insertRuleItems.AddRange(rules);
					}
				}
			}

			//if you find any rules then use them.
			if (insertRuleItems.Any()) {
				//setup context
				InsertOptionsRuleContext ruleContext = new InsertOptionsRuleContext {
					Item = args.Item,
					InsertOptions = args.Masters
				};
				//get those rules
				RuleList<InsertOptionsRuleContext> rules = RuleFactory.GetRules<InsertOptionsRuleContext>(insertRuleItems, "Rule");
				if (rules != null) {
					rules.Run(ruleContext);
				}
			}
		}

		/// <summary>
		/// This gets the first child matching the provided template name
		/// </summary>
		public static Item ChildByTemplate(Item Parent, string Templatename) {

			IEnumerable<Item> children = from child in Parent.GetChildren() 
										 where child.TemplateName.Equals(Templatename) 
										 select child;

			return (children.Any()) ? children.First() : null;
		}

		/// <summary>
		/// this checks if the current item's template id matches the given template id
		/// </summary>
		protected static bool IsTemplateID(Item item, string targetTemplateId) {
			return item.TemplateID.ToString().Equals(targetTemplateId);
		}

		/// <summary>
		/// this gets any website root item from the ancestors of the given item, if there is one.
		/// </summary>
		protected Item GetSiteItem(Item item) {
			return item.Axes.SelectSingleItem("ancestor-or-self::*[@@templatename='" + this.Website + "']");
		}
	}

}


 
I wanted to note that I embedded the insert options under the rules folder because I wanted to leave the door open to the possibility that more types of rules other than insert options could be supported for the sites.

After I'd implemented this I found the All About Insert Options article by John West which was a good source of information as well.

Adding Fields To The Link Database

$
0
0

This article details how to add fields to the Link Database, how Sitecore uses the FieldTypes.config to add items to the Link Database, issues with the DelimitedField type and how to create your own custom type to use in the FieldTypes.config.

Adding Fields to the Link Database

Setting up new or custom fields is pretty straight forward. In my initial research I found this article by John West to be quite helpful. There's two parts to getting a field setup with the link db: setting up the config entries and rebuilding the link db. You can add entries to the FieldTypes.config file or your own custom config file if you've got one setup. The config entries are laid out like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore> 
		<fieldTypes> 
			<fieldType name="Field Name" type="Namespace.Class, BinaryName" /> 
		</fieldTypes> 
	</sitecore> 
</configuration> 

Then you've got to rebuild the Link Database by going into the Sitecore 'start' menu and opening up the control panel. Select the 'Database' section and then click on the 'Rebuild the Link Database' option. You'll get to pick which databases to run against. Typically you'll want to choose 'Master' or 'Web'. This will go through your content tree and check all fields so it may take a while. It may also fail so be prepared for that. Future incremental changes to the field entries will be handled by events defined in the Sitecore config.

How Sitecore uses the FieldTypes.config

The config entry has two attributes: 'name' and 'type'. The 'name' value should match your field name since it will be used to tell Sitecore which fields it should handle. The 'type' value on the other hand doesn't have to match your class. The 'type' does have a few requirements that aren't detailed anywhere I'm aware of. I'll list them below and explain in more detail after. The 'type' is used largely to reflect how the value is stored, not how is displayed. Based on all existing sitecore types it appears this 'type' should be a subclass of the Sitecore.Data.Fields.CustomField. The field also needs to have a constructor that takes a parameter whose type is Sitecore.Data.Fields.Field. This class will also need override and implement the CustomField's ValidateLinks method since it is an empty method.

So to better explain how Sitecore is using this config definition to add the field to the Link Database let's use the item:saved event. When you edit, for example, a Treelist field, and click the save button, the value is stored as a pipe delimited list of item ids. This event also triggers the item:saved pipeline event. By default, Sitecore's FieldTypes.config uses a Sitecore.Data.Fields.MultilistField to manage the Treelist field. This first calls the method Sitecore.Links.ItemEventHandler.OnItemSaved. It begins by using the Sitecore.Links.ItemLinks class to loop through all the fields on the item you're saving and checks to see if each field has an entry in the FieldTypes.config. With those that do, the entry from the config is used to create an object of the type specified with a constructor that takes a single Sitecore.Data.Fields.Field argument by way of the Sitecore.Reflection.ReflectionUtil. If the field provided doesn't have a constructor that matches that condition, it will be skipped. After the list of fields is compiled they are looped through again and their ValidateLinks method is called. This method will add the links it finds to a list that will be then added to the Link Database.

Some Issues with the Sitecore.Data.Fields.DelimitedField

While I was working to get the Field Suite fields added to the Link Database, I attempted to use the existing FieldTypes.config entries. For example I used the Sitecore.Data.Fields.MultilistField for the Field Suite Treelist since it was used for the Sitecore Treelist field. What I found when I began debuggging was that when I had set the source field for the Field Suite Treelist it would not be added to the link db. I pinpointed the issue being the definition for the Sitecore.Data.Fields.DelimitedField.GetDatabase() method. The logic checks the 'Source' field for a 'DataSource'. If that is found it will also try to find the 'Databasename' from the 'Source' field which makes sense. The problem is that if it doesn't find a 'Databasename' it fails to fallback on the default InnerField.Database value. It will instead return null even though there is a valid value to return. The only way to work around this is to extend the Delimited Field and override the ValidateLinks and GetDatabase methods and return the InnerField.Datbase by default. I later found an article by Alex Shyba that details this workaround. I've also submitted a support ticket to sitecore to let them know of the issue so it'll likely be patched in future releases.

Adding a Custom Type to the FieldType.config

Since I'm currently using the Field Suite fields and I needed to patch the previous issue so I began looking into the fields that Sitecore was using. I ended up building two classes to support the fields: IDField and IDListField. For the IDField I researched how the Sitecore.Data.Fields.ReferenceField and LookupField worked since they both managed fields that stored a single item id as a string. For the IDListField I researched the Sitecore.Data.Fields.Multilist and DelimitedFields. I largely used what I found in both sets of classes and trimmed out what I didn't need and simplified their logic. There are really only a few things that your custom class will need which I've explained above but want to reiterate:

  • It should be a subclass of the Sitecore.Data.Fields.CustomField class.
  • It should override the CustomField ValidateLinks method.
  • It should have a constructor that takes a Sitecore.Data.Fields.Field object as an argument.

Sitecore Caching Manager Updated

$
0
0

I've recently updated the Caching Manager module on the Sitecore Marketplace. The improvements were focused on the UI which I've made to mimic the native design of the content editor. It's a lot more compressed and easy to use. The different cache regions are broken into separate tabs and parts of the form are broken into sections. You can collapse the sections by double clicking on the title bar like you can in the content editor. There's use of Sitecore's icons and the buttons are better defined and the font size is smaller so that more fits on a screen.

There was also an update by Mark Ursino who merged all the functionality into one page for an even simpler deployment.

Overall I feel that this provides a much more understandable layout. I'm not sure how everyone else is using it but if you like the earlier version you will definitely like the update.

Also to note the project source has moved since the shuttering of the Trac site and it now lives on GitHub.

Here's some screenshots of the new layout:

Search Cache

Site Cache

Database Cache

Access Result Cache

Miscellaneous Cache

Sitecore Data Importer Updated

$
0
0

I recently spent some good quality time working to update the Data Importer Module. It's an incredibly useful tool and I wanted to address some feedback about it so that users can better understand what it can do and how you use it.

The module was originally developed to import content from existing websites into Sitecore. I had to pull all types of information from raw SQL databases used by other CMS'. It can save a lot of time and effort doing these imports because you create the import definitions they are stored in the Sitecore content tree and you can rerun it as needed. It helps when you realize that you need to tweak the information you've just imported and delete and re-import the content. It's also useful for when you've got to import a dataset initially to start a project and then import more later to keep up to date with what's been added to the existing site.

So for changes, I went through the module and updated UI so that it uses Sitecore's native design which should make it feel more at home. I merged the Sitecore import and SQL import dropdowns and now all import types are run in the same way and only the implementation of the class differentiates them. I also revamped the code a lot. I added better messaging so that users know if they've forgot to fill out fields and I dealt with a lot of choke points to catch exceptions caused by missing information that would force you to restart the module and otherwise become frustrated with using it. I also put a lot more thought into making the system more modular so that it's much more simple to add custom data providers and custom field types. Altogether it should be much easier to customize. As a final addition I added a import mapping for MySQL databases after building it for an example in one of the videos. There are MySql.Data binary files that are installed with the package to support this.

To ease the learning curve I also created some walkthrough videos at the advice of good friend, Mark Ursino, to help get you familiar with the module and how to work with it. Here's the rundown:

If you're interested in working with the source code it's moved to GitHub since Trac was shut down.

Here's a screenshots of the module's new UI:

data importer

Transform Web Controls In Sitecore Rich Text Editor

$
0
0

This article is a follow-up to an article written previously by John West about embedding web controls in Rich Text Editors.

I spent some good time using web controls in the Rich Text Editor to allow content editors to implement complex functionality inline without a lot of strain. It's a step up from using code snippets where you don't want a content editor to have to switch to html mode to set html attributes or other behind-the-scenes elements.

For those of you not familiar with web controls or server controls it's basically a custom asp.net tag in the form <customTagPrefix:customTagName>. I use dialog windows to create them and, as long as your web.config setting "HtmlEditor.SupportWebControls" is set to true, the Sitecore Rich Text editor will display them with an icon like so:

 web control

But to make sure the tag isn't just spit out and treated as if it were any other html tag you'll need to transform it.

As I mentioned John West has already fully explained how to render fields by setting up a custom pipeline event handler. I ended up recently finding out that this case doesn't cover when you place the content field value on the page without using a Field Renderer. Let's say you have a field that, for whatever reason, you decided to place on a page as plain text by using it's Field.Value property. You'll find that the web controls won't be rendered and if you look at the html source you'll see them there staring you in the face plain as day. What you can do is add into your vast library of code, a utility method (pulled and modified from John West's post) that takes in the string value and transforms the web controls then returns a string value. This will turn all those fun webcontrols from boring html into amazing displays of genius. 

The method is as follows:

public static string TransformWebControls(string args) {
	System.Web.UI.Page page = new System.Web.UI.Page();
	page.AppRelativeVirtualPath = "/";
	System.Web.UI.Control control = page.ParseControl(args);
	return Sitecore.Web.HtmlUtil.RenderControl(control);
}

Ok...Carry on.

Browser Language Detection In Sitecore

$
0
0

I recently had a request to detect the browser language for a multilingual site I support. Now this certainly isn't anything new and here's the canonical/obligatory John West post written about it. I'm not trying to rewrite what he's done. My solution is different and deals with different needs. The requirements I had were:

*set the context language to the browser language if the site has item versions in that language
*if the user changes the language through a dropdown selector maintain the new language settings

These seemingly innocuous requirements add more complexity than you'd expect. The challenge was really around maintaining the language once it's been set and to not continue changing to the browser settings. I accomplished this by only using the browser settings if the lan

guage cookie had not been set. This makes the detection more passive in that it's only really determined on the first page load. It prevents a lot of jumpy behavior between languages when someone browses back to the homepage which doesn't contain the language code in the url.

So to implement this I overrode the LanguageResolver in my include config:

<processor type="Sitecore.Pipelines.HttpRequest.LanguageResolver, Sitecore.Kernel">
	<patch:attribute name="type">MyLibrary.Pipelines.HttpRequest.LanguageResolver, MyLibrary</patch:attribute>
</processor>

Next is the class that rewrites the functionality of the stock Sitecore LanguageResolver. I typically don't extend these classes since they may change over time and they also contain so many private methods that I basically end up having to copy most of it anyway. Also bear in mind that my system stores the supported languages for a site within the site's settings so I can compare against that stored list. This code was modified from my version to check to see if the homepage has a version of the language. This could be volatile since a site might not support a language just because there is a version of the homepage in that language. So you should consider modifying the GetBrowserLang() method to query the correct language versions for each of your sites if you can't make those assumptions.

This also uses a method of retrieving the home item for a site that assumes the SiteContext.RootPath and SiteContext.StartItem fit neatly together to make a sitecore item path. Make sure this is going to work for your system.

namespace MyLibrary.Pipelines.HttpRequest {
	
	using Sitecore;
	using Sitecore.Diagnostics;
	using Sitecore.Globalization;
	using System;
	using System.Web;
	using Sitecore.Pipelines.HttpRequest;
	using System.Threading;
	using System.Linq;
	using Sitecore.Data.Managers;
	using Sitecore.Data;
	using Sitecore.Data.Fields;
	using System.Collections.Generic;
	using Sitecore.Sites;
	using Sitecore.Data.Items;
	using Sitecore.Security.Domains;
	using Sitecore.Web.UI.Sheer;
	using Sitecore.Layouts;
	using Sitecore.Configuration;
	
	public class LanguageResolver : HttpRequestProcessor {

		private Language GetQStringLang(HttpRequest request) {
			Language language;
			string str = request.QueryString["sc_lang"];
			if (string.IsNullOrEmpty(str))
				return null;
			
			if (!Language.TryParse(str, out language))
				return null;
			
			return language;
		}

		private Language GetBrowserLang(HttpRequest request) {
			
			//get list of browser langs
			string[] bLangs = request.UserLanguages;
			if (bLangs == null || !bLangs.Any())
				return null;

			//get the list of supported langs for this site
			Item homeItem = GetHomeItem();
			if (homeItem == null)
				return null;

			//loop through browser languages and see if the site supports any
			List<string> langIDs = new List<string>();
			foreach (string s in bLangs) {

				//split any browser weighting that is applied ie: nl;q0.5
				string[] x = s.Split(new [] {";"}, StringSplitOptions.RemoveEmptyEntries);
				string langCode = (x.Any()) ? x.First() : string.Empty;
				if (string.IsNullOrEmpty(langCode))
					continue;
				
				//try to get the language
				Language language = null;
				if (!Language.TryParse(langCode, out language))
					continue;
				
				//then try to get the language item id from the language or two letter iso code
				ID langID = LanguageManager.GetLanguageItemId(language, Sitecore.Context.Database);
				if (ID.IsNullOrEmpty(langID)) {
					//sometimes the language found is slightly different than official language item used in SC
					language = LanguageManager.GetLanguage(language.CultureInfo.TwoLetterISOLanguageName);
					langID = LanguageManager.GetLanguageItemId(language, Sitecore.Context.Database);
				}

				//checks to see if the language item is a valid sitecore item
				if (ID.IsNullOrEmpty(langID))
					continue;

				//if you've got a version in this language then use that language
				Item langVersion = Sitecore.Context.Database.GetItem(homeItem.ID, language);
				if (langVersion != null)
					return language;
			}
					
			return null;
		}

		private Language GetLanguageFromRequest(HttpRequest request) {
			//check querystring
			Language qsLang = GetQStringLang(request);
			if (qsLang != null)
				return qsLang;

			//only check if you're on the frontend and the cookie hasn't been set
			if (IsNotSitecore() 
				&& Sitecore.Context.PageMode.IsNormal
				&&!IsLanguageSetFromCookie()) {
				//then check browser language settings
				Language browserLang = GetBrowserLang(request);
				if (browserLang != null)
					return browserLang;
			}

			//default
			return Context.Data.FilePathLanguage;
		}

		public override void Process(HttpRequestArgs args) {
			Assert.ArgumentNotNull(args, "args");
			Language languageFromRequest = this.GetLanguageFromRequest(args.Context.Request);
			if (languageFromRequest != null) {
				Context.Language = languageFromRequest;
				Tracer.Info("Language changed to \"" + languageFromRequest.Name + "\" as the query string of the current request contains language (sc_lang).");
			}
		}

		protected static Item GetHomeItem() {
			SiteContext sc = Sitecore.Context.Site;
			return Sitecore.Context.Database.GetItem(string.Format("{0}{1}", sc.RootPath, sc.StartItem));
		}

		protected static bool IsNotSitecore() {
			return (!Sitecore.Context.Domain.Name.ToLower().Contains("sitecore"));
		}

		protected static bool IsLanguageSetFromCookie() {
			string c = Sitecore.Web.WebUtil.GetCookieValue(Sitecore.Context.Site.GetCookieKey("lang"));
			return (!String.IsNullOrEmpty(c));
		}
	}
}

Include File References Patch Issue

$
0
0

*Update 2

I also found that there was an issue with the <using> statements as well that required an attribute to be added. When I opened the Configure-Assign (Insert Options) or tried to add from template, I would get the following exception:

The type or namespace name 'EventArgs' could not be found.

Once I added an arbitrary attribute to the <using> tag it was fine.

*Update 1

So Sitecore Support got back to me pretty quickly and told me that if I just add an attribute that makes the reference unique that's how to solve the issue. Here's the relevant code:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<ui>
			<references>
				<reference comment="MyLibrary">/bin/MyLibrary.dll</reference>
			</references>
		</ui>
	</sitecore>
</configuration>

The system I manage was originally built on Sitecore 5.3 and not all the latest and greatest methodologies are being used so when I have time I try to update the system. This time, while working to migrate all the customizations to the web.config to an include file I ran into a slight snag. My longtime friend and confidant, the ever charming System Exception or collegially known as the Yellow Screen of Death:

The type 'System.ComponentModel.IComponent' is defined in an assembly that is not referenced. You must add a reference to assembly 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

Usually they're not too difficult to root out but this one really confused me. I mean who doesn't have the 'System' library referenced. I'm pretty sure it's included by default. Anyway after finding nothing useful on google I was left to my only option. Go back to the original web.config and move the changes over line by line until I hit the issue.

Sure enough the issue resided with the <references> section. Now looking back I guess I should've guessed this would be the problem but hindsite, well, it just is what it is. I checked the rendered config on the show config page [/sitecore/admin/showconfig.aspx] and found that my inserted reference was showing up before the System.dll reference. Ah, that's the issue. It's not being included in the right order. Okay so I just need to put it last and I'll be fine.

Now typically when you patch config something you're telling it to insert before or after something but in this case there are no attributes to use to tell it where to insert. I know I read that I could tell it what position to insert into by giving it the position. It's not optimal but let's just see. To my chagrin, this didn't work. I checked the rendered config on the show config page again and it was showing up correctly but it was still throwing the error. Ok, so let's try and add an attribute like I've seen on John West's blog and insert it after that item using the attribute query. It was rendered right but still throwing the error.

With my options running out my only remaining choice was to just put it back into the web.config. Not what I wanted to do but it did work. For the time being I'll be adding a ticket to the support site to make sure Sitecore knows about it. Who knows maybe it's already a known issue.


Site Specific Insert Option Rules For Sitecore

$
0
0

I recently had the need to add a new template type for a single site on my multi-site system and wanted to be able to provide insert options for this template on content folders only in this site. The content folders are a global template and are shared across multiple sites so adding this new template type to the insert options of the content folder was not a viable option.

I know, from previous experience that rules engine would be great solution for this but there were a few additional constraints that I had which the default settings didn't provide for. I wanted to limit the scope of the rule to work only on items in that site. Using the default setup I would have to specify that the selected item would need to be an ancestor of the web root item but this isn't a preferable option since I would need to add this condition to every rule.

I wanted to store the rules at least in folders separated by site and I figured I could use this separation to know when to run the rules and save the added conditions. By default the rules for all insert options are stored in a single folder within Sitecore in /sitecore/system/rules/insert options/rules, which means that rules for all sites is in one folder. This isn't optimal because the list could potentially become very long and difficult to know which rules are for which sites. Also I wanted to store the rules for each individual site separately so that when a site is taken down and removed from the system all the rules would be easy to remove along with the site specific layouts, sublayouts and templates. I potentially could add subfolders to the insert rules. I would have to override the default processor so that it would look recursively into folders but I would also have to rely on finding the folder to match the site name which could be flaky since someone could misspell the folder. I decided to store the rules for the site below the site root at the same level at the home node for the site.

site specific rules folder

Now that I had decided where to store the rule I had to figure out the best way to apply them. I could either add the rules to the template or override the processor. Adding the rules to the template meant that it would be cluttered with rules from all sites and would require maintenance if/when sites were added or removed. I decided that the best option was to override the processor. This would allow me to query for the rules from the site start path by matching it to the current item and then add any insert options that are stored there.

I found that the pipeline processor uiGetMasters/RunRules is what needs to be overridden. I pulled the source from a decompiler and noticed that it was directly looking for the global folder. I dug a little deeper and found that its taking all the children of the folder and passing it to an overload of the GetRules methods that takes an IEnumerable<Item>. From here I knew that I still wanted to include the global items, I just wanted to add a few more things to the enumeration. I used the context item that is passed in with the mastersArgs and checked the ancestors for a site root node. If one is found then I look through its children for a rules folder and then check the rules folder for any insert option rules in its ancestors. I then add these to the list and then run rules like it was originally.

Here's the code for my override of the RunRules class: 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Pipelines.GetMasters;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;
using Sitecore.Rules.InsertOptions;
using Sitecore.Rules;

namespace MySite.Pipelines.GetMasters
{
	public class RunRules
	{
		protected string RulesFolder = "RulesFolder";
		protected string InsertOptionsRule = "{664E5035-EB8C-4BA1-9731-A098FCC9127A}";
		protected string Website = "WebsiteRoot";

		// Methods
		public void Process(GetMastersArgs args) {
			
			//check if null
			Assert.ArgumentNotNull(args, "args");
			if (args.Item == null)
				return;

			//init start list
			List<Item> insertRuleItems = new List<Item>();
			
			//skip permissions
			using (new SecurityDisabler()) {

				//get the default rules
				Item DefaultRules = args.Item.Database.GetItem("/sitecore/system/Settings/Rules/Insert Options/Rules");
				if (DefaultRules != null)
					insertRuleItems.AddRange(DefaultRules.Children);

				//get any site specific rules.
				Item SiteRoot = GetSiteItem(args.Item);
				if (SiteRoot != null) {
					Item RulesFolder = ChildByTemplate(SiteRoot, this.RulesFolder);
					if(RulesFolder != null) {
						var rules = from val in RulesFolder.Axes.GetDescendants()
									where IsTemplateID(val, this.InsertOptionsRule)
									select val;
						if (rules.Any())
							insertRuleItems.AddRange(rules);
					}
				}
			}

			//if you find any rules then use them.
			if (insertRuleItems.Any()) {
				//setup context
				InsertOptionsRuleContext ruleContext = new InsertOptionsRuleContext {
					Item = args.Item,
					InsertOptions = args.Masters
				};
				//get those rules
				RuleList<InsertOptionsRuleContext> rules = RuleFactory.GetRules<InsertOptionsRuleContext>(insertRuleItems, "Rule");
				if (rules != null) {
					rules.Run(ruleContext);
				}
			}
		}

		/// <summary>
		/// This gets the first child matching the provided template name
		/// </summary>
		public static Item ChildByTemplate(Item Parent, string Templatename) {

			IEnumerable<Item> children = from child in Parent.GetChildren() 
										 where child.TemplateName.Equals(Templatename) 
										 select child;

			return (children.Any()) ? children.First() : null;
		}

		/// <summary>
		/// this checks if the current item's template id matches the given template id
		/// </summary>
		protected static bool IsTemplateID(Item item, string targetTemplateId) {
			return item.TemplateID.ToString().Equals(targetTemplateId);
		}

		/// <summary>
		/// this gets any website root item from the ancestors of the given item, if there is one.
		/// </summary>
		protected Item GetSiteItem(Item item) {
			return item.Axes.SelectSingleItem("ancestor-or-self::*[@@templatename='" + this.Website + "']");
		}
	}

}


 
I wanted to note that I embedded the insert options under the rules folder because I wanted to leave the door open to the possibility that more types of rules other than insert options could be supported for the sites.

After I'd implemented this I found the All About Insert Options article by John West which was a good source of information as well.

Linq to Sitecore

$
0
0

I wanted to write up an article that elaborates on a presentation I gave on some but not all of the new features that will be upcoming in Sitecore 7. Mostly I'm going to focus on the updates around Lucene search but I'll also touch on some of the other features as well. This will be a longer article so be warned.

I'd also like to preface this article by saying that one of the driving forces behind much of the change in Sitecore 7 is an increasing need to support large data sets. And don't dismiss this release thinking that there isn't a lot of value if you're system isn't that large. You don't need to be very big to benefit from a faster search utility or the new features it includes since it doesn't really take much more than a few thousand items to really find Lucene beneficial.

What's Changing

The new system is highly geared toward improved search and as a shining example of that is how the Item Buckets are now baked into the content editor. Buckets are a great way to manage large sets of content considering the suggested limit of around 100 child items per item in Sitecore. But let's also consider that aside from massive quantities of data, not all content needs to be stored structurally in a tree. There's plenty of use cases where you just need to store information and will deal with selecting and displaying it based on the value of the fields and not on how they're stored. to assist with this, you'll see that there are new editor tabs attached to what seems like all items in the tree, that allow you to search for content in the context of the item selected. They've also included new field types that allow users to search for items which is great because item lists can grow long fast inside those tiny [treelist, droplink etc.] windows.  

Hot Swappable Indexes

One of the major changes to Lucene is that you can now configure it to build the index in a separate folder so that it doesn't interfere with the existing index. If you've ever found yourself with no results or a filelock exception due to Lucene deleting the index before rebuilding, you'll be interested in this. It's not setup by default but you can configure it by changing the configuration setting for the Lucene Provider. Here's how it will look:

<indexes hint="list:AddIndex"> 
	<index id="content_index" type="Sitecore.ContentSearch.LuceneProvider.SwitchOnRebuildLuceneIndex, Sitecore.ContentSearch.LuceneProvider"> 

The way this works is that the two folders for the index will be named like so: index_folder, index_folder_sec. The "_sec" stands for secondary. The provider will use the most up to date folder and will rebuild into the other. It should be noted that after you make the change to use the SwitchOnRebuildLuceneIndex, you will need to perform a rebuild twice on the index as per the provided documentation.

Interchangeable Indexes

The next set of updates are regarding the indexing source itself. Lucene can be now replaced with an implementation of SOLR that can be setup and configured remotely and queried in it's vastness very quickly. I still have questions about if the Lucene indexer can be configured to run on a separate machine in a similar vein but I don't currently know. This of course will require some training time to understand how to set that up but if you're building something that needs that kind of scale I'd guess you'll just be glad it's possible.

The new API

There's also a new search API "Sitecore.ContentSearch" that's unofficially being called "Linq to Sitecore" and includes an "ORM" feature that allows you to cast your search results to your existing class-model structures (assuming you have one) such as Custom Item Generator or Glass Mapper. Actually the casting is similar to that of the Glass Mapper in how you decorate fields to identify which fields should be popuplated from corresponding fields in the search result. You may now have questions, as I do, about where Alex Shyba's amazing AdvancedDatabaseSearch/Sitecore.SearchContrib stands. I think this may end up replacing it but again I don't know. It seems like this new search has in many ways learned from what he did and taken it a bit further with it's syntax. I did find this bit in the documentation explaining that Sitecore does want to begin phasing out the older search methods:

"The Sitecore.Data.Indexing API was deprecated in Sitecore CMS 6.5 and in 7.0 it has been completely removed. The Sitecore.Search API works with Sitecore CMS 6 and 7, but is not recommended for new development in version 7.0. Developers should use the Sitecore.ContentSearch API when using Sitecore Search or Lucene search indexes in Sitecore 7.0."

Configuration and Code Samples

Okay so now that I've covered a bit about what's happening I'll get the "how" side of things. I wanted to see how much effort was involved in using the new search features to see if I would be completely relying on it for all my data querying needs or just supplementing the existing methods. I started by setting up a local instance and creating some templates, content items and a sublayout. The next piece is to change the search configuration file located at /App_Config/Includes/Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration to tell it to store my field values in the index by changing storageType="NO" to storageType="YES":

<fieldType fieldTypeName="single-line text" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String"   settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />

With that set you'll then need to run a rebuild of the index which is now available in many ways but I used the one available through the Control Panel in the Content Editor. Then I found an example of how to query from the "Developer's Guide To Item Buckets and Search" document provided to me by Sitecore. I've altered it a bit to show how to make the query more generic for use in returning results of any given type and to also see what it's limits were:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Security;
using Sitecore.Data;
using Sitecore.Data.Items;

namespace TestsiteSC7.Web.layouts {
	public partial class PeoplePage : System.Web.UI.Page {

		#region Utility Methods

		protected StringBuilder log = new StringBuilder();
		protected void Log(string title, string message) {
			log.AppendFormat("{0}:{1}<br/>", title, message);
		}

		#endregion Utility Methods

		#region Page Events

		protected void Page_Load(object sender, EventArgs e) {

			//show indexes
			//ShowIndexes();

			//demo hydration
			//GetPeople();

			//show all fields
			GetResult();
			
			//write log
			ltlOut.Text = log.ToString();
		}

		#endregion Page Events

		#region Search Methods

		protected void ShowIndexes() {
			Dictionary<string, ISearchIndex> indexes = SearchManager.SearchConfiguration.Indexes;
			Log("Number of Indexes", indexes.Count.ToString());
			foreach (KeyValuePair<string, ISearchIndex> p in indexes)
				Log("Index", p.Key);
		}

		protected void GetPeople() {
			List<Person> people = PerformSearch<Person>();
			Log("Number of People", people.Count().ToString());
			foreach (Person i in people) {
				Log("name", i.Name);
				Log("address", i.Address);
				Log("occupation", i.Job);
				Log("_fullpath", i.Path);
				Log("parsedlanguage", i.ParsedLanguage);
				Log("_template", i.template);
				Log("_name", i.OtherName);
				Log("", "");
			}
		}

		protected void GetResult() {
			//filters down to a single result
			IEnumerable<MySearchResultItem> results = PerformSearch<MySearchResultItem>()
				.Where(a => a.Name.Equals("Jim"));
			Log("Number of results", results.Count().ToString());
			foreach (MySearchResultItem m in results) {
				Log(m.Name, string.Empty);
				foreach (KeyValuePair<string, string> f in m.fields)
					Log(f.Key, f.Value);
			}
		}

		private List<T> PerformSearch<T>() where T : AbstractResult, new() {
			//the fields are managed in:
			//			/App_Config/includes/Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration
			//this index is managed in:
			//			/App_Config/includes/Sitecore.ContentSearch.Lucene.Index.Master.config
			//this settings for indexing in:
			//			/App_Config/includes/Sitecore.ContentSearch.config
			var index = SearchManager.GetIndex("sitecore_master_index");
			using (var context = index.CreateSearchContext(SearchSecurityOptions.EnableSecurityCheck)) {
				var queryable = context.GetQueryable<T>()
					.Where(a => a.template.Equals(AbstractResult.TemplateID));
				
				//work with items inside the context
				T instance = new T();
				//display count
				instance.HandleResults(queryable);

				//need to send to list before the using outside the context
				return queryable.ToList();
			}
			//exception thrown when working with IQueryable object outside of context
			//Accessing IndexSearchContext after Commit() called
		}

		#endregion Search Methods
	}

	public interface IResult {
		string template { get; set; }
		string Name { get; set; }
	}

	public abstract class AbstractResult : IResult {
		
		public string Name { get; set; }
		[IndexField("_template")]
		public string template { get; set; }

		public static readonly string TemplateID = "fc110b3df82c4b0eabc580a4f185ea1d";

		public abstract void HandleResults(IQueryable<AbstractResult> results); 
	}

	public class Person : AbstractResult {

		#region properties
		
		[IndexField("_name")]
		public string OtherName { get; set; }

		public string ParsedLanguage { get; set; }
		
		[IndexField("_fullpath")]
		public string Path { get; set; }
		
		public string Address { get; set; }
		
		[IndexField("occupation")]
		public string Job { get; set; }

		#endregion properties

		public override void HandleResults(IQueryable<AbstractResult> results) {
			IEnumerable<Person> people = (IEnumerable<Person>)results;
			HttpContext.Current.Response.Write("People count:" + people.Count().ToString());
		}
	}

	public class MySearchResultItem : AbstractResult {

		#region properties

		// Fields
		public readonly Dictionary<string, string> fields = new Dictionary<string, string>();
		// Will match the myid field in the index
		public Guid MyId { get; set; }
		public int MyNumber { get; set; }
		public float MyFloatingPointNumber { get; set; }
		public double MyOtherFloatingPointNumber { get; set; }
		public DateTime MyDate { get; set; }
		public ShortID MyShortID { get; set; }
		public ID SitecoreID { get; set; }
		
		// Will be set with key and value for each field in the index document
		public string this[string key] {
			get {
				return this.fields[key.ToLowerInvariant()];
			}
			set {
				this.fields[key.ToLowerInvariant()] = value;
			}
		}

		#endregion properties

		public override void HandleResults(IQueryable<AbstractResult> results) {
			IEnumerable<MySearchResultItem> people = (IEnumerable<MySearchResultItem>)results;
			HttpContext.Current.Response.Write("Results count:" + people.Count().ToString());
		}		
	}
}

What to Expect

I will state for the record that it doesn't take much effort to get results. In fact you may get more than you bargained for. By default you'll get all results in the index, which include everything and the kitchen sink. I ended up with standard values items as results even after filtering by template id. The key thing to note here is that Lucene is returning results and Sitecore is then using the field values to fill properties on the class you provide. It will fill your object with data whether or not it makes sense to. I guess this is no different than other ORMs but keep in mind that you will be best served by filtering out the results thoughtfully. What I will recommend is that you create several small indexes that are focused just on the template/content types you're trying to retrieve otherwise you may have other data comingling into your results. Making smaller indexes will also keep the amount of memory and CPU usage to a minimum when rebuilding indexes and will limit the scope of what is affected.

The Code Walkthrough

Now I'll delve a bit into the code itself. I've created an interface IResult, an abstract class AbstractResult and two classes: Person and MySearchResultItem (MySearchResultItem is actually pulled from the documentation). The use of the interface was to require that all results have certain fields. The abstract class was to give enough substance to a generic result class to allow the search method to call a method with the result set.

Sitecore is going to use the search result values to populate your classes after a search is run and you'll notice that on the Person class there are fields decorated to identify which values should be put into what property. You also won't have to add the decoration if the class property matches the name of a field in a result. I haven't yet tested how field names with spaces will be handled but I imagine that the spaces will just be removed or replaced.  Here's some different examples:

//setting Sitecore provided fields
[IndexField("_fullpath")]
public string Path { get; set; }

//fills itself by matching names
public string Address { get; set; }

//populating a template field into an unrelated property name
[IndexField("occupation")]
public string Job { get; set; }

It will help to become acquainted with the fields you have available to you. The MySearchResultItem has an example of property called fields that will get populated by Sitecore with all the field names/values. You can use this as a guide to see what's available to you. By default, it contains some Sitecore information like template id and item path among others:

public readonly Dictionary<string, string> fields = new Dictionary<string, string>();
// Will be set with key and value for each field in the index document
public string this[string key] {
	get {
		return this.fields[key.ToLowerInvariant()];
	}
	set {
		this.fields[key.ToLowerInvariant()] = value;
	}
}

When I got to actually operating the search, my goal was to push as much generic search functionality into a single method and allow the calls for each type to be extremely simple. Here's how the search is initiated in my examples.

List<Person> people = PerformSearch<Person>();
IEnumerable<MySearchResultItem> results = PerformSearch<MySearchResultItem>();

The PerformSearch is simple and reusable. It also takes a Generic type that is of type AbstractResult and has an empty constructor. Here's the signature for that method:

private List<T> PerformSearch<T>() where T : AbstractResult, new() {}

It starts by getting the index and setting up a using context. In this case the security is enabled so Sitecore will filter results with permissions in mind:

var index = SearchManager.GetIndex("sitecore_master_index");
using (var context = index.CreateSearchContext(SearchSecurityOptions.EnableSecurityCheck)) {

I'm using the built in master database index and I've filtered by the template id provided by the abstract class. Which does work well. The search and filter largely occur on a single line, which is also really quite impressive.

var queryable = context.GetQueryable<T>().Where(a => a.template.Equals(AbstractResult.TemplateID));

The GetQueryable<T> is the part where Sitecore will fill your class type with the result values and the .Where() is the Linq to Sitecore which does all the work in the Lucene/SOLR index. You will be able to use the alternate syntax as well:

var queryable = from T a in context.GetQueryable<T>
		where a.template.Equals(AbstractResult.TemplateID)
		select a;

The beauty here is that you're offloading a lot of the filtering directly to Lucene instead of having to sort/filter/trim a humungous list of items in the .NET then cache it. You should know that not all queryable methods are implemented but what is available certainly isn't a short list. The supported methods are: Where, Select, Take, Skip, Page, OrderBy, OrderByDescending, ThenBy, ThenByDescending, First, FirstOrDefault, ElementAt, Last, LastOrDefault, Single, SingleOrDefault, Any and Count. It's definitely got some teeth.

I also wanted to point out that you're really only able to manipulate the search result in it's current form while you're inside the using context. If you try to return an IEnumerable result and then try to loop through the results you'll get an exception like:

Accessing IndexSearchContext after Commit() called

This is likely because of how IEnumerable behaves in that the items are only queried when the GetEnumerator is called and in this case the objects that are used to crunch the data have been disposed of through the IDisposable using context. You do have two options to deal with this. You'll either want to pass in a handler method or have one available on your Type(T) class, as I did or you can convert the result set to a List<T>. This will force the data to be crunched while the context is still available. Here's the example of the handler being called on the AbstractResult class which is implemented separately in the subclasses.

T instance = new T();
instance.HandleResults(queryable);

I also return the result set as a List and do work on the results in the calling method.

return queryable.ToList();

My results are then iterated through and have some of the field data printed to a log for display:

Log("Number of People", people.Count().ToString());
foreach (Person i in people) {
	Log("name", i.Name);
	Log("address", i.Address);
	Log("occupation", i.Job);
	Log("_fullpath", i.Path);
	Log("parsedlanguage", i.ParsedLanguage);
	Log("_template", i.template);
	Log("_name", i.OtherName);
	Log("", "");
}

Hopefully this new search, with it's simplified API and multi-foldered index will be big enough improvements to gain the trust of the development community and get rapid adoption. I think you'll be able to rely on it whether you've got a small or large system, as it's quite fast and powerfull.

One More Thing

There is also a nice update to Sitecore regarding the caching properties on sublayouts. You will be able to configure the cache to clear when the index is updated. This may need some customization for multi-site instances since you (and I) may not want all html cache removed when you publish a single item.

Synopsis

Sitecore has now brought Lucene out of the shadows and integrated it into the system as a much more visible and reliable utility to build your solutions on. The new API is definitely a breath of fresh air and will hopefully, lower the barrier for entry for developers to configure and use it and allow us to build much more efficient applications.

Site Scraper

$
0
0

I just finished cleaning up a utility I built a few years ago and pushed it up to GitHub. I often found myself needing to have an html export of some sites so that design vendors could update the css/js, or some sites were being moved to different systems in another part of the company. I've cleaned up the design so that it's less distracting and updated how it works so that you get what you want in the end: a clickable website in a folder.

Here's a screenshot:

site scraper

There's two parts that I built: the Xenu list processor and the link processor. I use the Xenu site crawler to first get a list of all the css, js, pdfs, images and page urls that the site has on it. Then I export that list to a tab separated file. With the first form, I remove all the extra information from the Xenu link list. The form also creates a new file that can be loaded into the drop-down list of the second form. There's also a few options you have to work with such as include/exclude filtering and separating the links for files, images and pages into separate files. Sometimes it's easier to process long lists of files separately. I found it useful so it's in there.

The second form will take the list generated by the first form, or really any list of links you provide it and will then request for the content of that page and save it to a file. The files generated will be foldered to match the existing site and will replace all the internal site links to relative paths so that when you open the files in a browser you can click to other pages. You do of course have options so that you can opt to not update the urls to relative paths, you can save the pages down as a different file type, you can set what the default page name will be and you can append the querystring to the filename so you can capture separate states of an individual page if you need to.

I've found it incredibly useful and hopefully other people will also.

Field Suite Images Field

$
0
0

I've been using the FieldSuite, which was built by Sitecore MVP, superhero and all around good guy, Tim Braga, for about 8 months now and have come to really like a lot of its featues but there was one field that I hadn't used: The "Field Suite Images Field". When I thought I'd finally look into it, the documentation had a few details about customizing the control but not about using the field which gave me the opportunity to do a quick write-up. So first I tinkered with it until I figured out how to get it working and then figured it would be a good idea to write up a quick post about it.

The basic premise is an extension of the built-in Sitecore image field which allows you to select a single image. With this Field Suite Images Field you can select multiple images, albeit,  one at a time.

First you'll want to have a media library folder with some images in it. I've also created a nested folder to show that it support it.

media library structure

When you add the field to a template, you'll want to set the field source to a media library folder where you'll want it to pull images from.

field definition

Once you've setup the template, next you'll create an item in the content tree and you'll see that the Images Field has a row of buttons to help manage the value.

field empty

If you click on the "Link Item" button this allows you to use a drop tree field to select an image.

field select item

After you select an image you click save and you'll return to the field value with the image set.

field save

field single selected

If you continue to "Link" more images you'll see them appear in a horizontal list with their publish status (another Field Suite feature) below.

field multiple selected

From here you can select any individual item in the field and edit, go to the item, remove the item or reorder the item in the list.

field image select

When it comes time to access the field value, it's stored as a pipe delimited list of Sitecore ID's so it should be fairly easy to work with.

field raw value

Sitecore Custom User Properties

$
0
0

In a recent Sitecore build I had to store the pages that a user saved as a "Favorite" throughout the site. The site was an intranet so everyone had a user within Sitecore, which helped. The next question was where to store the information. I wanted it to be stored along with local user data but wasn't sure if I was going to need to extend the user object and create extra fields or if I could leverage the existing structure. I ended up looking through a lot of the user properties and scouring the cookbooks and eventually I found what I was looking for in the security api cookbook: Custom Properties.

You can access and set custom properties on a Sitecore user. It stores these custom properties in a NameValueCollection so you can get and set the values with a key. With that settled, I knew I'd only need to be storing the item ID's for each page. I ended up creating a wrapper class to manage the key for the favorites and to help manage the string value as a comma-delimited list.

Here's the code I used:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Security.Accounts;

namespace MyLibrary {

	public static class UserExtensions {

		public static readonly string custPropKey = "user-favorites";
		public static readonly string custPropDelim = ",";

		#region Add Favorite

		public static void AddFavorite(this User u, ID pageID) {
			AddFavorite(u, pageID.ToString());
		}

		public static void AddFavorite(this User u, Item page) {
			AddFavorite(u, page.ID);
		}

		public static void AddFavorite(this User u, string pageID) {
			IEnumerable<string> f = GetFavorites(u);
			List<string> l = f.ToList();
			//skip if already added
			if (l.Contains(pageID))
				return; 
			l.Add(pageID);
			SetFavorites(u, l);
		}

		#endregion Add Favorite

		#region Remove Favorite

		public static void RemoveFavorite(this User u, ID pageID) {
			RemoveFavorite(u, pageID.ToString());
		}

		public static void RemoveFavorite(this User u, Item page) {
			RemoveFavorite(u, page.ID);
		}

		public static void RemoveFavorite(this User u, string pageID) {
			IEnumerable<string> f = GetFavorites(u);
			List<string> l = f.ToList();
			if (!l.Contains(pageID))
				return; 
			l.Remove(pageID);
			SetFavorites(u, l);
		}

		#endregion Remove Favorite

		#region Set Favorites

		public static void SetFavorites(this User u, List<string> pageIDs) {
			StringBuilder sb = new StringBuilder();
			foreach (string s in pageIDs) {
				if (sb.Length > 0)
					sb.Append(custPropDelim);
				sb.Append(s);
			}
			u.Profile.SetCustomProperty(custPropKey, sb.ToString());
			u.Profile.Save();
		}

		#endregion Set Favorites

		#region Get Favorites

		public static IEnumerable<string> GetFavorites(this User u) {
			string s = u.Profile.GetCustomProperty(custPropKey);
			IEnumerable<string> split = s.Split(new string[] { custPropDelim }, StringSplitOptions.RemoveEmptyEntries).ToList();
			return split;
		}

		public static IEnumerable<ID> GetFavoriteIDs(this User u) {
			IEnumerable<string> s = GetFavorites(u);
			return (s.Any()) ?
				from string id in s
				where ID.IsID(id)
				select ID.Parse(id) 
				: Enumerable.Empty<ID>();
		}

		public static IEnumerable<Item> GetFavoriteItems(this User u) {
			IEnumerable<ID> s = GetFavoriteIDs(u);
			List<Item> returnList = new List<Item>();
			if (s.Any()) {
				var items = from ID i in s
							select Sitecore.Context.Database.Items[i];
				foreach(Item i in items){
					if(i != null)
						returnList.Add(i);
				}
			}
			return returnList;
		}

		#endregion Get Favorites

		#region Is Favorite

		public static bool IsFavorite(this User u, string pageID) {
			IEnumerable<string> s = GetFavorites(u);
			List<string> l = s.ToList();
			return l.Contains(pageID);
		}

		public static bool IsFavorite(this User u, ID pageID) {
			return IsFavorite(u, pageID.ToString());
		}

		public static bool IsFavorite(this User u, Item page) {
			return IsFavorite(u, page.ID);
		}

		#endregion Is Favorite
	}
}

Now knowing this and looking back, there's a lot of other useful things you can store for a user to help manage users who access different sites but end up in the same extranet security domain. There's also a lot of registration information that can be stored without having to modify anything. Here's to forward thinking in design.

Creating Sitecore Sublayouts Dynamically

$
0
0

Preface

On a recent project I was looking to replace some existing infrastructure that built a sidebar on a page using a list of content items (non-page items) that were selected by the content editor. The existing system was using a single sublayout which had several templated server controls that defined the display for different types of content. This was a global resource and any individual site that wanted to modify the display would have to copy the file in it's entirety even if it only wanted to modify a single line. It also didn't provide a default display for items that weren't explicitly defined. Only predefined types were displayed so there was some confusion as to why some things wouldn't be displayed.

There's, of course, a lot of ways to do this, especially within Sitecore. My plan was to keep the sidebar sublayout as the parent display object but I wanted to be able to create individual sublayouts for each content type. Then I would add each to the parent sublayout. I didn't want to load the ascx files as user controls because I wanted to be able to control the settings such as datasource, parameters, caching etc. This lead me to adding Sitecore sublayout objects to the page dynamically. It also allowed me to follow certain conventions and build the sublayouts with respect to the datasource instead of the context item.

Now if a single site wanted to override how the contact information was displayed, it would only have to override that single sublayout and it wouldn't affect anything else. It also allowed me to remove a lot of duplication in the system.

Solution

In this case I had business logic that already determined which display objects were used for which content types, I modified it to replace the server control references with paths to the sublayout files and then set that path on the Sitecore sublayouts. Here I also set the datasource on the sublayout to the item ID of the content item, I pass in a parameter to let each sublayout know what it's sort order is and then (I don't but it's possible) set the cache settings as well. Here's the ascx method I use that creates the sublayout and attaches it to the page:

protected void AddFeature(Item i, string filePath) {
	Sitecore.Web.UI.WebControls.Sublayout s = new Sitecore.Web.UI.WebControls.Sublayout();
	s.Path = filePath;
	s.DataSource = i.ID.ToString();
	s.Parameters = string.Format("Order={0}", Order.ToString());
	s.Cacheable = true;
	s.VaryByData = true;  
	this.Controls.Add(s);
	Order++;
}

I'm also using a BaseSublayout class to manage the accessor properties for the Datasource and Parameters

public class BaseSublayout : System.Web.UI.UserControl { 
	private Item _DataSourceItem; public Item DataSourceItem {  
		get {   
			if (_DataSourceItem == null)    
				_DataSourceItem = Sitecore.Context.Database.GetItem(((Sublayout)Parent).DataSource);   
			return _DataSourceItem;  
		}  
		set { _DataSourceItem = value;  } 
	}
	private NameValueCollection _Parameters; 
	public NameValueCollection Parameters {  
		get {   
			if (_Parameters == null)    
				Parameters = Sitecore.Web.WebUtil.ParseUrlParameters(((Sublayout)Parent).Parameters);   
			return _Parameters;  
		}  
		set {   _Parameters = value;  } 
	}
}

Sitecore Manager and Helper Classes

$
0
0

As a follow up to my Sitecore Util Classes post, I randomly did a search through the Sitecore 6.4.1 kernel for Manager and Helper classes and as it turns out this time I hit a gold mine. Like my previous article I'll only touch on them briefly because there's just too many to go into any real depth but I wanted to at least put it out on the internets to raise a little awareness on these hidden gems in Sitecore. I left out any Interfaces, Abstract classes or classes without any public methods. In total I found 55 manager classes and 15 helper classes.

Manager Classes

Sitecore.Caching.CacheManager
public static void ClearUserProfileCache(string userName)
public static Cache[] GetAllCaches()
public static Cache FindCacheByName(string name)
public static HtmlCache GetHtmlCache(SiteContext site)

Sitecore.Data.Archiving.ArchiveManager
public static Archive GetArchive(string archiveName, Database database)
public static int GetArchiveCount(Database database)
public static List<Archive> GetArchives(Database database)

Sitecore.Data.DataManager
public DataUri[] GetItemsInWorkflowState(WorkflowInfo info)
public WorkflowInfo GetWorkflowInfo(Item item)
public Item[] SelectItems(string query)

Sitecore.Data.Fields.FieldTypeManager
public static CustomField GetField(Field field)
public static FieldType GetFieldType(string name)

Sitecore.Data.Managers.HistoryManager
public static HistoryEntryCollection GetHistory(Database database, DateTime from, DateTime to)
public static void RegisterItemCreated(Item item)
public static void RegisterItemDeleted(Item item, ID oldParentId)

Sitecore.Data.Managers.IndexingManager
public static string GetUpdateJobName(Database database)
public static void UpdateIndex(Database database)

Sitecore.Data.Managers.ItemManager
public static Item AddFromTemplate(string itemName, ID templateId, Item destination)
public static bool DeleteItem(Item item)
public static ChildList GetChildren(Item item)
public static VersionCollection GetVersions(Item item, Language language)

Sitecore.Data.Managers.LanguageManager
public static Language GetLanguage(string name, Database database)
public static bool IsLanguageNameDefined(Database database, string languageName)
public static bool RegisterLanguage(string name)
public static Language DefaultLanguage

Sitecore.Data.Managers.TemplateManager
public static bool ChangeTemplate(Item item, TemplateChangeList changes)
public static Template GetTemplate(ID templateId, Database database)
public static TemplateDictionary GetTemplates(Database database)
public static bool IsFieldPartOfTemplate(ID fieldId, Item item)
public static bool IsTemplate(Item item)

Sitecore.Data.Managers.ThemeManager
public static string GetIconImage(Item item, int width, int height, string align, string margin)
public static string GetImage(string path, int width, int height)
public static string GetLanguageImage(Language language, Database database, int width, int height)

Sitecore.Data.Proxies.ProxyManager
public static Item CopyVirtualTreeRoot(Item item, Item target)
public static Item GetItem(ID itemId, Language language, Sitecore.Data.Version version, Database database)
public static Item GetRealItem(Item item, bool preserveState)
public static bool HasVirtualItems(Item item)

Sitecore.Data.Serialization.Manager
public static void DumpItem(Item item)
public static void DumpRole(string rolename)
public static void DumpTemplates(Database database)
public static Item LoadItem(string path, LoadOptions options)
public static Role LoadRole(string path)

Sitecore.Data.StandardValuesManager
public static string GetStandardValue(Field field)
public static bool IsStandardValuesHolder(Item item)

Sitecore.Data.Validators.ValidatorManager
public static ValidatorCollection BuildValidators(ValidatorsMode mode, Item item)
public static void UpdateValidators(ValidatorCollection validators)
public static void Validate(ValidatorCollection validators, ValidatorOptions options)

Sitecore.Eventing.EventManager
public static void QueueEvent<TEvent>(TEvent @event)
public static void RaiseQueuedEvents()
public static SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler)
public static void Unsubscribe(SubscriptionId subscriptionId)

Sitecore.Events.Hooks.HookManager
public static void LoadAll()

Sitecore.Jobs.JobManager
public static Job GetJob(Handle handle)
public static bool IsJobRunning(string jobName)
private static void RemoveJob(Job job)
public static void Start(Job job)

Sitecore.Links.LinkManager
public static string ExpandDynamicLinks(string text)
public static UrlOptions GetDefaultUrlOptions()
public static string GetDynamicUrl(Item item, LinkUrlOptions options)
public static string GetItemUrl(Item item, UrlOptions options)
public static RequestUrl ParseRequestUrl(HttpRequest request)

Sitecore.Presentation.ControlManager
public static IWebControl GetControl(string controlUri, Dictionary<string, string> parameters)

Sitecore.Presentation.PresentationManager
public static ItemPresentation GetPresentation(Item item)

Sitecore.Publishing.DistributedPublishingManager
public static PublishStatus GetStatus(Handle statusHandle)
public static Handle QueuePublish(PublishOptions[] options)
public static void StartPublishing(StartPublishingRemoteEvent @event)
public static void UpdatePublishStatus()

Sitecore.Publishing.PreviewManager
public static string GetShellUser()
public static void ResetShellUser()
public static void SetUserContext()

Sitecore.Publishing.PublishManager
public static void AddToPublishQueue(Item item, ItemUpdateType updateType)
public static bool CleanupPublishQueue(DateTime to, Database database)
public static void ClearPublishQueue(Database database)
public static ItemList GetPublishingTargets(Database database)
public static PublishStatus GetStatus(Handle handle)
public static Handle RebuildDatabase(Database source, Database[] targets)

Sitecore.Resources.Media.MediaManager
public static Sitecore.Resources.Media.Media GetMedia(MediaItem item)
public static string GetMediaUrl(MediaItem item)
public static string GetThumbnailUrl(MediaItem item)
public static bool HasMediaContent(Item item)
public static bool IsMediaRequest(HttpRequest httpRequest)

Sitecore.Resources.Media.MediaPathManager
public static string GetExtension(string file)
public static string GetMediaFilePath(string itemPath, string extension)

Sitecore.Search.SearchManager
public static Index GetIndex(string id)
public static object GetObject(SearchResult hit)

Sitecore.Security.AccessControl.AccessRightManager
public static AccessRight GetAccessRight(string accessRightName)
public static AccessRightCollection GetAccessRights()
public static bool IsApplicable(AccessRight accessRight, ISecurable entity)

Sitecore.Security.AccessControl.AuthorizationManager
public static AccessResult GetAccess(ISecurable entity, Account account, AccessRight accessRight)
public static AccessRuleCollection GetAccessRules(ISecurable entity)
public static bool IsAllowed(ISecurable entity, AccessRight right, Account account)
public static bool IsDenied(ISecurable entity, AccessRight right, Account account)
public static void RemoveAccessRules(ISecurable entity)
public static void SetAccessRules(ISecurable entity, AccessRuleCollection rules)

Sitecore.Security.Accounts.RolesInRolesManager
public static void AddRolesToRole(IEnumerable<Role> memberRoles, Role targetRole)
public static IEnumerable<Role> FindRolesInRole(Role targetRole, string roleNameToMatch, bool includeIndirectMembership)
public static Role GetEveryoneRole()
public static IEnumerable<Account> GetRoleMembers(Role role, bool includeIndirectMembership)
public static bool IsUserInRole(User user, Role targetRole, bool includeIndirectMembership)

Sitecore.Security.Accounts.UserManager
public static int GetUserCount()
public static IFilterable<User> GetUsers()

Sitecore.Security.Authentication.AuthenticationManager
public static User BuildVirtualUser(string userName, bool isAuthenticated)
public static User GetActiveUser()
public static bool Login(string userName, string password, bool persistent)
public static void Logout()
public static void SetActiveUser(User user)

Sitecore.Security.Authentication.HttpAuthenticationManager
public static bool Authenticate(HttpRequest request)
public static bool IsAuthorized(HttpRequest request)

Sitecore.SecurityModel.DomainManager
public static void AddDomain(string domainName)
public static bool DomainExists(string domainName)
public static DomainConfiguration GetConfiguration()
public static Domain GetDomain(string name)
public static Domain GetDomainForUser(string userName)

Sitecore.SecurityModel.License.LicenseManager
public static void BlockSystem(string reason)
public static void ResetLicense()
public static long GetDatabaseSize(Database database)
public static int GetEditorUserCount()

Sitecore.Shell.Applications.ContentEditor.Galleries.GalleryManager
public static void GetGallerySize(string id, ref string width, ref string height)
public static void SetGallerySizes()

Sitecore.Shell.Applications.ContentEditor.Gutters.GutterManager
public static List<ID> GetActiveRendererIDs()
public static List<GutterRenderer> GetRenderers()
public static string Render(List<GutterRenderer> gutterRenderers, Item item)

Sitecore.Shell.Feeds.ClientFeedManager
public static ClientFeed GetFeed(string feedname)
public static SyndicationFeed GetFeedsAreDisabledFeed()

Sitecore.Shell.Framework.Commands.CommandManager
public static Command GetCommand(string name)
public static CommandState QueryState(Command command, CommandContext context)
public static void RegisterCommand(string name, Command command)

Sitecore.Shell.Framework.Commands.OpenSecurityManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Shell.Framework.Commands.Shell.DomainManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Shell.Framework.Commands.Shell.RoleManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Shell.Framework.Commands.Shell.UserManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Sites.SiteManager
public static Site GetSite(string siteName)
public static SiteCollection GetSites()

Sitecore.Syndication.FeedManager
public static PublicFeed GetFeed(Item item)
public static string GetFeedUrl(Item feedItem, bool includeUserAuthentication)
public static string Render(SyndicationFeed feed)

Sitecore.Visualization.LayoutManager
public static Rendering GetRendering(string fullName, Database database)

Sitecore.Visualization.RuleManager
public static void Execute(object thisObject)
public static RuleSet GetRuleSet(object thisObject)

Sitecore.Web.Authentication.TicketManager
public static string CreateTicket(string userName, string startUrl)
public static bool Relogin(string ticketId)

Sitecore.Web.UI.HtmlControls.PageScriptManager
public bool AddScripts
public bool AddStylesheets
public List<ScriptFile> ScriptFiles
public List<StylesheetFile> StylesheetFiles

Sitecore.Web.UI.WebControls.AjaxScriptManager
public void Dispatch(string parameters)
public ClientCommand Alert(string text)
public ClientCommand Download(string file)
public ClientCommand SetInnerHtml(string id, string value)

Sitecore.Web.UI.WebControls.ContinuationManager
public static void Register(string key, ISupportsContinuation @object)
public Pipeline Start(Pipeline pipeline, PipelineArgs args)

Sitecore.Web.UI.WebControls.MessageManager
public object SendMessage(Message message)
public object SendMessage(object sender, string message)

Sitecore.Web.UI.XamlSharp.ControlManager
public static Control GetControl(ControlUri uri)
public static string GetControlUrl(ControlName name)

Sitecore.Web.UI.XamlSharp.Xaml.XamlManager
public static AttributeAssigner GetAttributeAssigner(System.Xml.XmlAttribute attribute)
public static Compiler GetCompiler(ControlDefinition definition)
public static ControlCompiler GetControlCompiler(CompilerOptions options)
public static Sitecore.Web.UI.XamlSharp.Xaml.Extensions.Extensions GetExtensions()

Sitecore.Web.UI.XmlControls.ModeManager
public static ModeMatch Match(ArrayList modes, HashList<string, object> requested)

Sitecore.WordOCX.WordOCXUrlManager
public string GetDownloadLink()
public string GetEditLink()
public string GetPreviewLink()
public string GetUploadLink()

Helper Classes

Sitecore.Common.PagingHelper
public List<TOutput> GetRecords(int pageIndex, int pageSize, IEnumerable<TInput> inputList, GetElements<TInput, TOutput> getElements, out int totalRecords)

Sitecore.Configuration.ProviderHelper
public string GetRootAttribute(string attributeName)

Sitecore.Data.DataProviders.SqlServer.SqlServerHelper
public static int GetInt(SqlDataReader reader, int columnIndex)
public static long GetLong(SqlDataReader reader, int columnIndex)

Sitecore.Links.LinkDatabaseHelper
public static void UpdateLink(Item item)

Sitecore.Publishing.PublishHelper
public bool CanPublish(ID itemId, User user, ref string explanation)
public virtual PublishItemResult DeleteTargetItem(ID itemId)
public virtual Item GetSourceItem(ID itemId)
public virtual Item GetTargetItem(ID itemId)
public virtual Item GetVersionToPublish(Item sourceItem)
public PublishItemResult PublishSharedFields(Item sourceItem)
public bool SourceItemExists(ID itemId)
public bool TargetItemExists(ID itemId)

Sitecore.Reflection.NexusHelper
public static FastInvoker GetInvoker(string typeName, string methodName, bool isStatic)

Sitecore.Security.AccessControl.FieldAuthorizationHelper
public override AccessRuleCollection GetAccessRules(Field field)
public override void SetAccessRules(Field field, AccessRuleCollection rules)

Sitecore.Security.AccessControl.ItemAuthorizationHelper
public override AccessRuleCollection GetAccessRules(Item item)
public override void SetAccessRules(Item item, AccessRuleCollection rules)

Sitecore.Security.Authentication.AuthenticationHelper
public virtual bool CheckLegacyPassword(User user, string password)
public virtual User GetActiveUser()
public virtual void SetActiveUser(User user)
public virtual bool ValidateUser(string userName, string password)

Sitecore.Security.SecurityHelper
public static bool CanRunApplication(string applicationName)

Sitecore.SecurityModel.License.RSAHelper
public string Decrypt(string message)
public string Encrypt(string message)

Sitecore.Text.NVelocity.VelocityHelper
public static string Evaluate(VelocityContext context, string template, string logName)
public static void PopulateFromSitecoreContext(VelocityContext context)

Sitecore.Web.IPAddresses.IPHelper
public static IPList GetIPList(System.Xml.XmlNode node)

Sitecore.Xml.Xsl.SqlHelper
public string connect(string connection)
public string fullsearch(string search, string table, string displayFields, string searchFields)
public string select(string sql)

Sitecore.Xml.Xsl.XslHelper
public virtual string Application(string name)
public virtual string day()
public virtual string displayname(XPathNodeIterator iterator)
public virtual string EnterSecurityState(bool enabled)
public virtual string ExitSecurityState()
public virtual string formatdate(string isoDate, string format)
public virtual string fullUrl(string fieldName, XPathNodeIterator ni)
public virtual string GetDevice()
public virtual Item GetItem(XPathNodeIterator iterator)


Designing a Page Editor Experience: 1 - Setting up the Code

$
0
0

Shout Out

As a shout out, this series started because I was recently speaking with Mark Ursino and he gave me a demonstration of a recent build where he had taken advantage of a few really slick features in Page Editor. I decided to do some research to see what Page Editor could really do. In doing my research I came across a lot of great posts that individually detail pieces of functionality, some of which I'd seen demonstrated at the last Symposium. For this post, I wanted to weave together as much of a cohesive picture along with some commentary and lots of referential links for developers to see a fuller Page Editor experience.

Preface

Before I begin, I want to bring to light some things to consider. The Page Editor has evolved to allow you to you store related content in DataSource fields on sublayouts as opposed to storing this information in fields, which is traditionally how many Sitecore sites were built. It's important to understand how the relationships between DataSources are stored in Sitecore and how they're changing between Sitecore 6.6 and 7. In 6.6 the DataSource for a sublayout is stored in the presentation details using the item's path (ie: /sitecore/content/home/pagename). As of 7 this was changed so that the DataSource is stored in the link database as an item ID. This is important for a few different reasons:

  • if you move, rename or remove a data source item, any sublayouts storing the item's path will not be updated, the values will not be easy to cleanup and if you don't code your pages correctly, you could have exceptions being thrown when your page tries to access fields on null objects.
  • as sublayouts are added to the site and items are created as data sources, if a user removes the sublayouts from a page, the items will likely not be removed with them and you'll have to figure out a process for garbage collection on this content.
  • if and when you upgrade from 6.6 to 7, consider that you will want to convert your system from storing the datasources in the presentation details to the link database. I'm not sure if there is any formal way to do this or not yet so do your research.

If you do choose to go ahead with 6.6 and store the DataSource information in the Presentation Details of a page, one thing you can do to help yourself is modify your system so that Sitecore stores the item ID's instead of their paths. This should at least make it somewhat easier to convert and more importantly much less prone to breakage.

Also, if you rely on Lucene search, consider that when you're storing the relationships between a page and it's content in the Presentation Details or link database, you're going to face new challenges to associate this content to a page. With 6.6 you'll need to figure out how to extract the DataSource ID's from the xml in the Layout field and with 7 you'll have to figure out how to gather the information from the link database.

Getting Started

Ok, with that said, whether or not you choose to store content relationships in the presentation details, Sitecore's Page Editor, has a lot to offer. When you get started thinking about designing an experience for Page Editor your journey will start with the code itself. There's a lot of great posts coming out focusing on component based approach including this really thorough series. This push has been for reuse and modularity but also more importantly to support the tools provided by Sitecore for tracking and personalization. What this means is that when you're designing your templates you should do so with consideration towards breaking up the page into reusable components whose content is interchangeable. Components are a general reference to user control sublayouts, xsl renderings and library server controls. I prefer working with sublayouts so going forward my examples will use them but there are other options.

On each of the sublayouts (renderings or server controls), to make the content fields accessible you should be using FieldRenderers. They live in the Sitecore.Web.UI.WebControls namespace and there are several controls built specifically for different field types: Date, Link etc. To learn more about these controls you can bone up a bit by perusing the Presentation Component Reference(System Overview), the Presentation Component API Cookbook(In-Depth Developer Reference) and the Presentation Component Cookbook(Working inside Sitecore)
Otherwise if you need to customize the output you may implement your own by extending the FieldControl class or higher(IE: FieldRenderer).

Once you've wired up your UI, following the path of the sublayout design patterns emerging, you'll want to setup a base class for your sublayouts that provide access to the sublayout parameters and datasource(as opposed to using the Sitecore.Context.Item). Below I've provided a sample base class that provides access to a DataSourceItem, ContextItem and a DataSourceOrContext. DataSourceOrContext will fall back to the ContextItem if the DataSourceItem is null. The reason for is is because by default, FieldRenderers select the Sitecore.Context.Item as their datasource but that might not be preferred. By using this type of base class provides a way for sublayout to specifically decide which source it is targeting by overriding the PreferredDataSource which if untouched, defaults to DataSourceOrContext. This class then recursively sets the datasource for all FieldRenderers on the sublayout. I sourced the logic for this from this great post. As an example, if you change the PreferredDataSource item to use only the DataSourceItem, the recursive function will hide the controls if the DataSourceItem is null so that the FieldRenderer's won't attempt to display data from the Sitecore.Context.Item.

public class BaseSublayout : System.Web.UI.UserControl { 
	private Item _DataSourceItem; 
	public Item DataSourceItem {  
		get {   
			if (_DataSourceItem == null)    
				_DataSourceItem = Sitecore.Context.Database.GetItem(((Sublayout)Parent).DataSource);   
			return _DataSourceItem;  
		}  
		set { _DataSourceItem = value;  } 
	}

	public Item ContextItem {  
		get {   
			return Sitecore.Context.Item; 
		}	
	}

	private Item _DataSourceOrContext;
	public Item DataSourceOrContext {
		get {   
			if (_DataSourceOrContext == null)    
				_DataSourceOrContext = (DataSourceItem != null) ? DataSourceItem : ContextItem;   
			return _DataSourceOrContext;  
		}
	}

	private NameValueCollection _Parameters; 
	public NameValueCollection Parameters {  
		get {   
			if (_Parameters == null)    
				Parameters = Sitecore.Web.WebUtil.ParseUrlParameters(((Sublayout)Parent).Parameters);   
			return _Parameters;  
		}  
		set {   _Parameters = value;  } 
	}

	public virtual Item PreferredDataSource { get { return DataSourceOrContext; } }

	protected void ApplyDataSource() {
		foreach (Control c in Controls)
			RecurseControls(c, PreferredDataSource);
	}

	private void RecurseControls(Control parent, Item dsItem) {
		string typeName = parent.GetType().FullName;
		if (dsItem == null) {
			parent.Visible = false;
		} else if (typeName.StartsWith("Sitecore.Web.UI.WebControls")) {
			if(typeName.EndsWith("FieldRenderer"){
				Sitecore.Web.UI.WebControls.FieldRenderer f = (FieldRenderer)parent;
				f.Item = dsItem;
			} else {
				Sitecore.Web.UI.WebControls.FieldControl f = (FieldControl)parent;
				f.Item = dsItem;
			}
		}

		foreach (Control c in parent.Controls)
			RecurseControls(c, dsItem);
	}

	protected virtual void Page_Load(object sender, EventArgs e) {
		ApplyDataSource();
	}
}

I'm also bootstrapping the recursive function in the Page_Load method so if you want to work within that method on the local instance you'll want to override the method but make sure you call the Page_Load method from the base class:

protected override void Page_Load(object sender, EventArgs e) {
	... your code ...
	base.Page_Load(sender, e);
} 

Designing a Page Editor Experience: 2 - Sublayout fields

$
0
0

After you've decided what components your pages will be broken up into and created the sublayouts (renderings or server controls) each extending your base class, you can begin working on the setup inside Sitecore by configuring your component item's fields and setting up component insert options. These settings are stored on the sublayout items themselves and I'll detail them here.

The "Description" field is largely for you to provide a description of what the control does. I'm not aware of any place it is displayed but it's possible that it has other uses.

The "Parameter Templates" is one of the most interesting concepts. The intention is to allow developers the ability to customize the user interface for entering parameter values. If the default key/value UI control doesn't meet your needs you can attach a template to provide you with additional fields that you do need. First create a new template.

parameter template create

Add some fields.

parameter template fields

And set the new template to inherit from the Sitecore template: /System/Layout/Rendering Parameters/Standard Rendering Parameters.

parameter template base template 

Then you'll want to set the parameter template field on the sublayout item

parameter template set template 

Please note this is not a comprehensive guide to creating a template. I'm going to skip adding icons, standard values tokens and help text for convenience here but you should not. Now when you're working with a sublayout and you click "Edit" on the sublayout in the Presentation Details or in the Page Editor More->Edit the Component Properties, you'll see the new field section.

parameter template edit fields 

These values will then be accessible through the Parameters property defined on the BaseSublayout class provided in the previous post

The "Open Properties After Add" field allows you to choose if the component properties (datasource, caching, params etc.) windows will be opened automatically when a content editor adds a sublayout to a page. It has values of "Default", "Yes" and "No".

open properties on add 

These value affect the behavior of the checkbox "Open the properties dialog box immediately" when you insert a component/sublayout

insert sublayout 

A "Default" value will allow the content editor to choose. A "Yes" value disables the checkbox and automatically checks the box while a "No" value also disables the checkbox and keeps it unchecked.

The "Customize Page" field is apparently a legacy field. Quoting from the Presentation Component Reference: "The Customize Page property in the Editor Options section of rendering definition items is a legacy replaced by rendering settings data templates." Basically if you haven't used it yet, don't start.

The "Editable" field will prevent a content editor from being able to insert the sublayout through the Page Editor although an admin still has access and can add the sublayout through Presentation Details.

The "Datasource Location" field is used similar to the source on a template field. It's the location in the content tree where Sitecore will start a TreeList for you to select a data source item from when you click a to set the DataSource field on a sublayout. By default if you insert a sublayout you will not be asked to select a datasource. You can optionally edit the component properties to do so but if you select an item for this field you'll automatically see a popup asking you to choose a data source for the sublayout you're inserting.  Here I'll set the datasource to an item just under the home item.

 datasource location set

You'll see that the result tree now starts at that item.

datasource location select
  
The "Datasource Template" field is used to limit what types of content can be selected as a datasource. When you're selecting the datasource, if you select an unsupported type you'll receive a nice friendly message.

datasource template error 

Unfortunately the value for this field is stored as an internal link field and you can only select one template type. That being said, you may only need to select one template but pray you never rename your templates because this link will not update automatically.

The "Compatible Renderings" field is intended for situations where you want to swap out an existing sublayout but keep the datasource. You'll want to take note that this works best by selecing renderings that have use the same Datasource Templates so that if you swap it out the sublayout doesn't throw errors because the sublayout is expecting a different data source item type. To set this up, you first select a sublayout from the compatible renderings field.

compatible rendering field 

Then in the Page Editor after you've inserted this sublayout and click on it, you'll see a red and green arrow button with tooltip text: "Replace with another component".

compatible rendering change 

This will popup a dialog window for you to select fromt he sublayouts you've chosen

compatible rendering sublayout

The "Page Editor Buttons" field will allow you to add buttons to the detail window that displays when you select a sublayout in the Page Editor. There are two types of buttons: WebEdit buttons makes calls to the commands from the commands config file and Field Editor Buttons open a window that let you edit a specific set of fields. By default the bar is pretty light and you're only provided WebEdit Buttons to choose from.

web edit button types 

Each has it's own unique function:

  • the insert button will give you a popup that allows you to insert items for the context item.
    web edit button insert 
  • the delete button allows you to delete the context item (as long as it isn't the site start item)
    web edit button delete 
  • the sort button allows you to sort the child items through buttons or a drag and drop interface (if the browser supports it)
    web edit button sort 
  • the move up/move down moves the context item up and down in the Sitecore tree

If and when you decide you need some custom functionality available or you want to open up access to fields that aren't for display such as metadata fields, you'll want to jump to the Core database. The templates for the two button types are the "WebEdit Button" or the "Field Editor Button" and are both stored under: /core/sitecore/templates/System/WebEdit. 

web edit button templates 

You'll want to be creating them under: /core/sitecore/content/Applications/WebEdit/Custom Experience Buttons. The fields for a WebEdit Button are Header, Icon, Click(name from command file), Tooltip and Parameters.

web edit fields 

If you want to create a Field Editor Button there's a bit of setup involved so I'll walk through the steps. I'll note that the fields accessed will be on the Datasource item if it's been set so in this case metadata fields are only going to be useful if the datasource or context item is a page.

  • First you'll need to define meta data fields on a template
    template fields
  • Then jump into the core database and drill down to: /core/sitecore/content/Applications/WebEdit/Custom Experience Buttons
  • Insert an item of template type: /core/sitecore/templates/System/WebEdit/Field Editor Button
    field editor template
  • Set the fields you want to edit on the item. If there are multiple, separate the field names using the pipe(|) delimiter
    field editor item
  • Jump back into the master database assign the button to a sublayout item
    edit sublayout buttons
  • Open up Page Editor and select a sublayout with the button on it
    editor button
  • Click the button and you'll see a popup with the fields available to edit.
    field editor

For another article that specifically caters to this feature, you can read up here.

The "Enable Datasource Query" field is available in Sitecore 7 and I to be honest I don't know what it does yet. It's likely related to the new field type that Sitecore is going to replace the Datasource field with that John West explains here.

As for the "Data" and "Caching" sections of the sublayout, I'm going to gloss over them since they're not likely to be used as often. They store the same component property values that you select when you add a sublayout to a page but this will set them globally for all uses. It's definitely possible that you could find this useful but generally you'll want to set these values specifically where they're applied to a page or standard values item.

The "Login control" field is one exception to the previous sections that I will explain. I dug into the Sitecore.Kernel and found that this is a level of security that allows you to show another sublayout if !Context.User.Identity.IsAuthenticated. To enable access, you can authenticate users with a couple of quick lines of code:

string domainUser = Sitecore.Context.Domain.Name + @"\username";
bool loginResult = AuthenticationManager.Login(domainUser, "password");

This can be used for an ad-hoc extranet which you can manage permissions for different groups. I have a more extensive guide for Setting up a Sitecore Extranet which explains the process in more detail.

Designing a Page Editor Experience: 3 - Placeholder Settings

$
0
0

Placeholder settings are used to create predefined insert options for components (sublayouts, renderings and web controls). You have two basic approaches to use: setting the placeholder key on the item's Placeholder Key field: 

placeholder settings global key 

or setting the key when you apply it to an item through it's presentation details.

placeholder settings local set

Setting the placeholder key on the placeholder settings item will set the component insert options of any placeholders with a matching key. This could be useful for global elements or in single site deployments where you're not concerned about managing individual settings for each site.

Setting the placeholder key on the standard values of a template will allow you to set the same group of component insert options on different templates or placeholders. This, I suspect, may be most useful especially in multi-site instances since it would allow you to specify the sublayouts used for your specific site's placeholder keys.

There are a two out-of-the-box placeholder setting items in Sitecore: content and webedit. You can incorporate these into your strategy or create your own. I'll demonstrate the latter. Browse to /master/sitecore/content/Layout/Placeholder Settings. Right-click on the placeholder settings item and insert a new Placeholder item.

placeholder settings insert

If you're going to set the placeholder key on the placeholder setting item you may want to name the item based on the content placeholder key it will be used for. On the other hand if you're going to be specifying the placeholder key in standard values, you'll want to name it based on its intended use. IE: Sidebar Controls, Header Content etc. The new item has four fields: Placeholder Key, Allowed Controls, Description and Editable.

As mentioned earlier, the Placeholder Key field is optional. If it is set Sitecore will apply the component insert options to any placeholders with a key that matches.

The Allowed Controls field allows you to select the components that you want to be available to an editor when they click on the "Add to here" buttons in the Page Editor. For this example I'm going to add my New Sublayout under allowed controls. So on the field I'll click the "edit" button and will receive this dialog window.

placeholder settings allowed

Then click "Ok" and you'll see that the field has selected the item.

placeholder setting fields

The Description field appears to be informational. I didn't find any place where it was visible other than on the control itself.

The Editable checkbox determines if the placeholder settings will be editable from the presentation details windows. Here I've unchecked the checkbox and you can see the edit option is greyed out. 

placeholder settings editable

I'll want to also add the placeholder setting to the standard values of an item. I'm going to open the presentation details and then I'll click on the edit button for the default device.

placeholder settings presentation details 

Then I'll select the Placeholder Settings section and click add.

placeholder settings add

From Here I can select the New Sublayout item and enter in "content" for the Placeholder Key.

placeholder settings local set 

Now if I go to the Page editor and click on the blue component button to insert a component. I'll see the "Add to here" buttons.

placeholder settings add component 

When I click on the "Add to here" button, I'll now only be given the New Sublayout as an option.

placeholder settings option

Designing a Page Editor Experience: 4 - Personalization and Multivariate Testing

$
0
0

One of the nicest features that came along in the last few Sitecore versions was the rules engine. Along with that power was the ability to harness the conditional sublayout rendering to change the datasource or component design based on specific visitor information, otherwise known as personalization. There's all kinds of identifiable markers that are available to you about your end users such as location and search keywords. You can use this information as is to create rules, you can create "profiles" which categorize your users to help you reach your intended audience and you can use those "profiles" to build "Engagement Plans". I don't plan on going into detail about how to create these profiles or engagement plans since it's a near science unto itself but I did find a quick guide on personalization terms and an excellent articles on setting up personalization profiles and creating engagement plans.

Another powerful feature is Multivariate Testing. Multivariate Testing allows you to determine which configuration for a component (datasource and design) is best meeting your goals. Again I'm not going to detail how to develop a strategy for testing because there are articles dedicated to explaining multivariate testing, and optimizing your testing.

If you're going to use the personalization or multivariate testing aspect of the rules engine, you will need to have a special Sitecore license since it's a separate product. If you don't have a license you'll be wondering why you don't see these options in your system.

Once you've decide on how you're going to target key demographic groups, you're ready to begin applying some of the rules to your components(sublayouts, renderings, web controls). These rules are applied in the presentation details window of an item. When you open up the presentation details window is that there are two new buttons: Presonalize and Test.

presentation details

Personalize

If you select the controls section then select a component and click on the Personalize button, you'll get the personalization window.

conditions

What you're seeing is a list of the conditions(rules) that will determine how and what the sublayout displays. You're provided a default condition which doesn't contain a condition or rule since it's the default condition and will be used when no other more specific condition is met. All conditions allow you to hide the component, personalize the content(change the datasource) or personalize the component(change the sublayout). If you choose to personalize the content, you'll be provided with another dialog window to choose a content item from a tree which is sourced from the sublayout's Datasource Location field.

set content

If you click the Enable personalization of component design, you'll see a Personalize Component section appear.

Enable personalization of component design

By clicking on the grey "..." button at the bottom of that block, you'll be prompted to select a new sublayout.

Enable personalization of component design

When you want to create a new condition, you'll start by clicking on the New Condition button. Another condition section will be added which you can then name.

new condition

The hide component and personalize content ares are provided but there are also a few other buttons. There is an action button which allows you to remove or rename the item.

actions

There's also an Edit button in the condition section.

edit condition

If you click the Edit button, you'll be shown another popup that will allow you to build a condition from a series of rules.

edit rules

Here I've chosen to build a condition for visitors from Boston searching for the text "Sitecore" and changed the datasource to child1.

conditions

Once I click ok, you'll move back a step and find that the icon for the sublayout you just personalized has just changed to display a "2" to indicate that there are now two conditions on it.

presentation details

All of this is of course accessible from the Page Editor as well and in some ways is easier to work with from the Page Editor. If you click on a sublayout, you'll see the border with the helpful toolbar but with the personalization license there are a few new items. First is the button to personalize the content.

page editor buttons

There's also a button to add A/B tests.

page editor button

Then there's a bar that helps you navigate the conditions associated with the item.

page editor button bar

The arrow allows you to change the condition your viewing so you can quickly scroll through them and see how they'll each look. There is a numerical indicator next to it so that you'll know what number you're on in the list. The drop down area provides a summary of the condition rules and allows you to jump directly to an individual condition. There's also an Edit Conditions button that will popup a dialog window to edit the conditions.

You can also set some specific personalization settings through the component properties which are available in the presentation details when you click to edit a component.

component properties

If you have the license for this but don't see the fields, you can try the solution described here.

Tests

If you select the controls section then select a component and click on the Test button, you'll get the variation window.

new test

If you click on the New Variation button, you'll see a new variation item appear which you can name and set the properties (datasource and sublayout) just like with personalization.

test

You'll need to have at least two test variations for the OK button to be accessible.

tests

Unlike when you personalization a control, you won't be provided any icon but you will notice that the personalize button is now greyed out.

greyed out personalize button

Again, you can work with these settings from within the Page Editor which can sometimes be easier.

test button bar

Unlike the personalization, you'll have a new toolbar in the Page Editor at your disposal: Testing.

test toolbar

In the toolbar at the top, there's a Components and Combinations field that will highlight the sublayout that it refers to on the page when you click on it.

select test

Tests must be started so when you're ready to run it you'll want to click on the Start Test button. When you do you'll be provided a friendly warning to make sure you want to proceed and ask you to provide a name for the test.

start test

All your tests will be stored in Sitecore and can be viewed in the Marketing Center application available from the Sitecore start menu.

marketing center

While the test is running you'll see the toolbar change and give you the ability to stop it with the Stop Test button.

run test

When you click on the stop test, you'll see a popup that will ask you how to handle the components being tested. The form will allow you to select which component setup you want to keep and the numbers along the right will indicate their performance based on your goals. Here they're all zero but the higher number would indicate better performance.

stop test

Choose a single item comprised of combinations.

stop test combinations

Or you can stop the test and revert to the original design by clicking on a text link at the bottom of the window. This last option will remove the tests from the sublayouts and return you to the original state of the component before you added tests.

Sheer UI: 1 - A Tale of Two Systems

$
0
0

Preface

My journey began with a rebuild of a Sheer UI Wizard. I didn't build it, but I was about to learn to. After finding a dearth of literature on Sheer and realizing the documentation was a little out of date (I'm guessing SC 4), I knew I was going to write something on it. When I began writing I thought I knew a fair amount about Sheer from my experience creating different utilities but as it turns out, I knew very little. Though, from some of the applications I've seen, there seems to be a few who have faced the abyss and become steely Sheer developers, I'm betting there are more like myself with passable knowledge but not the whole picture. There was a recently released video walkthrough from the 24 hours of Sitecore led by Robert Hock that details setting up a Sheer UI application that's also worth watching.

I should mention that Sitecore is currently moving away from Sheer toward using Speak for a UI framework. It's initial release is in Sitecore 7.1 but up until you make it there or for backward compatibility with existing tools, you'll still need to work with Sheer. 

Sheer UI

So what is Sheer UI? Sitecore, in all their industry, built a framework that is used for creating the UI of Sitecore itself. The applications, development tools, security editors among other things are built using XML Controls, although there are a few .aspx pages used also. They work similar to .NET pages in that they have a front and back end file, page lifecycle and events that you can work with. You can add to Sitecore's UI by creating your own XML controls for use in Applications, Item Editor Tabs, Rich Text Editor Buttons, overriding the existing controls and an infinite number of other ways.

XML Controls

XML controls are structured presentation XML markup and can also reference a class for functionality. Each XML node in that control, after the first and second root nodes are references to other XML controls or library web controls. Sitecore supports having more than one XML control per file but it's recommended to separate them into individual files. Sitecore stores it's controls in two locations: /sitecore/shell/Applications and /sitecore/shell/Controls

Now here's where the world that I knew, began to look like a pale blue dot. There really are two supported types of XML control syntax: The original "controls" and the newer "xamlControls". For the release of version 5.3, Sitecore redesigned how they managed XML controls altogether and added a new handler extension to resolve the new compilation model. There was also a XamlSharp.config file that exposed configuration options for many settings including the compiler itself. There was a lot of thought put into it and it was big improvement but I'm aware of no documentation for it.

Structure

So what do these XML controls look like? Here's an example of the original "control" type using common elements:

<control 
     xmlns:def="Definition" 
     xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense"> 

     <ControlName> 
          <Stylesheet Src="/sitecore modules/shell/YourApplication/css/style.css"/> 
          <Script Src="/sitecore modules/shell/YourApplication/js/script.js"/> 
          <FormPage> 
               <CodeBeside Type="Namespace.Class,Assembly"/> 
               <GridPanel Height="100%" Width="100%" runat="server"> 
                    <Border ID="Container" runat="server"> 
                         <Literal ID="ltl" Text="Hello" runat="server" /> 
                    </Border> 
               </GridPanel> 
          </FormPage> 
     </ControlName> 
</control> 

A "control" file's extension is .xml. It's root node name can be anything but the convention is to name it "control". On the "control" node you do need to have either of the two attributes shown in the example for Sitecore to recognize it as an XML control. The text of it should be copied verbatim.

Here's an example of the newer "xamlControl" using common elements:

<xamlControls 
     xmlns:x="http://www.sitecore.net/xaml" 
     xmlns:ajax="http://www.sitecore.net/ajax" 
     xmlns:rest="http://www.sitecore.net/rest" 
     xmlns:r="http://www.sitecore.net/renderings" 
     xmlns:xmlcontrol="http://www.sitecore.net/xmlcontrols" 
     xmlns:p="http://schemas.sitecore.net/Visual-Studio-Intellisense" 
     xmlns:asp="http://www.sitecore.net/microsoft/webcontrols" 
     xmlns:html="http://www.sitecore.net/microsoft/htmlcontrols" 
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 

     <XamlHelloWorld x:inherits="Namespace.Class,Assembly"> 
          <Sitecore.Controls.HtmlPage runat="server"> <Stylesheet runat="server" Src="/sitecore modules/Shell/ApplicationName/css/style.css" x:placeholder="Stylesheets"/> 
               <Script runat="server" Src="/sitecore modules/Shell/ApplicationName/js/script.js" x:placeholder="Scripts"/> 
               <GridPanel Height="100%" Width="100%" runat="server"> 
                    <Border ID="Container" runat="server"> 
                         <Literal ID="ltl" Text="Hello" runat="server" /> 
                    </Border> 
               </GridPanel> 
          </Sitecore.Controls.HtmlPage> 
     </XamlHelloWorld> 
</xamlControls> 

Unlike the older "control", "xamlControls" file extensions' are .xaml.xml and their root node name must be named "xamlControls" otherwise you'll be the beneficiary of a blue screen: Xaml files must have a root node named "xamlControls". Well technically it's yellow, but blue sounded better. The attributes on the root node are used to help with intellisense and to help Sitecore know where too look for different elements. The xmlns (XML Namespace) attribute in the opening xml tag defines the prefix and the namespace. The prefix is local to that xml node in the file so it can be anything but the namespace is important. The "Definition" namespace used on "control" items were built into the early compiler but the "xamlControls" uses namespaces that are defined in the XamlSharp.config file under the <attributeAssigners> section. Now I'm still a little fuzzy on the entirety of the inner workings of how the compiler uses all these elements to build controls but from the limited time I have, that seems to be what's going on.

For both types of controls, the first child of the root node is where the control name is defined. The name is important since Sitecore references the control by name. Each control should have a unique name because if there are duplicates only the first one found by Sitecore will be used and the rest will be ignored. In regular use, since control names should be unique, it may be advantageous to name the control with a namespace which appears to be the convention on newer controls.

You should understand how each types loads supporting classes. The "control" and "xamlControl" types can use def:inherits or x:inherits respectively. Also the "control" types support the <CodeBeside> tag to define a class but the "xamlControl" does not. I'm not sure why this is so, since other controls in the same namespace will work but I'm chalking this one up to the inner mysteries of the compiler. One thing I do know is that neither the "control" nor the "xamlControl" support each other's type as an inner control. Take the use of the <FormPage> and <Sitecore.Controls.HtmlPage> controls as an example. The <FormPage> is a "control" type that lives here: /sitecore/shell/controls/Standard/Form Page.xml and the <Sitecore.Controls.HtmlPage> is a "xamlControls" type that lives here: /sitecore/shell/Applications/Xaml/Controls/HtmlPage.xaml.xml. You could not use the <Sitecore.Controls.HtmlPage> in a "control" nor could you use the <FormPage> in a "xamlControl".

Rendering

Another improvement made, was how Sitecore loads the control. For both methods you will need to be logged in to use the tools because they both run on the shell website.

"controls" can be previewed by the url: http://localhost/sitecore/shell/default.aspx?xmlcontrol=CaseSensitiveControlName 

"xamlControls" can be viewed by the handler url: http://localhost/sitecore/shell/~/xaml/ControlName.aspx. In some contexts in Sitecore, there are also querystring parameters such as: ID, Language, Version and Database. This handler url is configured in the <customHandlers> Sitecore section of the web.config.  

When you load a Sheer XML control, Sitecore, after first checking for the control in cache, will convert the control into an interim class file that inherits fromSitecore.Web.UI.XmlControls.XmlControl for "control" types and Sitecore.Web.UI.XamlSharp.Xaml.XamlControl for "xamlControls" respectively. This class is then compiled into its own assembly, an instance created and then rendered to a page. The compiled classes only recompile when the XML file is modified sufficiently, allowing you the flexibility of modifying the layout without having to recompile your project.

Configuration

There's a few web.config settings that can be set to modify XML control behavior in your system. I'm not sure if this is universal to both systems or not.

  • disableXmlControls (<sites>): If set to true, loading Xml Controls as pages will be disabled.
  • XmlControls.ThrowException (<settings>): Specifies if an exception is thrown when an Xml Control can't be loaded. Errors are written to the log.
  • XmlControls.OutputDebugFiles (<settings>) : If true, the *.cs files will be saved in to the debug folders associated with the XmlControls

"control" configuration

If you're working with the original "control" types, you can optionally configure the using statements, assemblies and control sources that Sitecore will insert when converting the XML controls into classes. Here's the sitecore web.config sections where you can manage these settings:

  • /sitecore/ui/using
  • /sitecore/ui/references
  • /sitecore/controlSources

Sitecore uses the sitecore/controlsources section of the web.config to define what libraries and folders are used to search for the controls used in an XML file. The default web.config includes these namespaces:

  • Sitecore.Web.UI.HtmlControls
  • Sitecore.Web.UI.WebControls
  • Sitecore.Shell.Web.UI.WebControls
  • Sitecore.Shell.Applications.ContentEditor
  • Sitecore.Shell.Web.Applications.ContentEditor
  • Sitecore.WebControls
  • System.Web.UI.WebControls
  • System.Web.UI.HtmlControls
  • Sitecore.Web.UI.Portal
  • ComponentArt.Web.UI

 and loads XML control files from these directories:

  • /sitecore/shell/override
  • /layouts
  • /sitecore/shell/controls
  • /sitecore/shell/applications
  • /sitecore modules

"xamlControls" Configuration

If you're working with the new "xamlControls" there is an /App_Config/XamlSharp.config file where settings are configured. One of the improvements using this newer system is that, similar to many other aspects of Sitecore, you have the ability to override Sitecore's default behavior such as the control compilers. You can also define custom folders and namespaces where controls can be used from. I'll give a brief rundown on what each section controls.

Compilers

The compilers sections defines the class that renders the XML Control files into a renderable control. This is where Sitecore has the ability to inject a handful of attribute features, such as class overriding and passing in parameters

ControlCompilers

The control compilers are the definitions of classes that are used to find the library control to use to render each control tag type in the XML markup.

Extensions

Extensions are custom tags that affect the control you're working on. The extensions can modify the attributes on the parent control, provide xsl-like if/else/for-each controls and even set local parameters.

AttributeAssigners

Attribute Assigners allow you to set attributes that aren't properties on the control but will be rendered by the Sitecore's compiler.

AttributeEvaluators

Attribute Evaluators allow you to write c# expressions inside attribute tags that get rendered when the control is compiled.

Sources

Sources define the folders and class libraries that are used to support the controls used in the XML.

The default XamlSharp.config namespaces included are:

  • Sitecore.Web.UI.HtmlControls
  • Sitecore.Web.UI.WebControls
  • System.Web.UI.WebControls
  • System.Web.UI.HtmlControls

and it includes these folder paths for Xaml Controls:

  • /sitecore/shell/Applications
  • /sitecore modules

DefinitionCreators

Definition Creators define the file extension that is used to identify XML controls.

HtmlControls

Html Controls are the html tags and what library control is used to render them. Anything that isn't defined on the list, should pass through as is.

Creating Custom Controls

When you create XML controls, consider putting them in your own folder under /sitecore modules/shell/ folder, which is already included by the web.config and XamlSharp.config. And for consistency, try to name the file, the XML control name and class name the same, otherwise it will be confusing to use.

The preferred base class for "control" types were Sitecore.Web.UI.Sheer.BaseForm and the preferred base class for "xamlControls" isSitecore.Web.UI.XamlSharp.Xaml.XamlMainControl.

You can create your own types or new folder locations, just make sure to update the web.config or XamlSharp.config to reference your library or folder so that Sitecore knows to look there for them. Since, as I previously mentioned, Sitecore references controls by their names and will use the first it finds, ignoring duplicates, you could override existing controls with your own by placing the config reference to your folders before the existing references.

Tutorial Sample

There is a tutorial Xaml control that lives under /sitecore/shell/Applications/Xaml. The url to view the control in action is this:http://localhost/sitecore/shell/~/xaml/Sitecore.Xaml.Tutorials.Index.aspx. Some of the pages were throwing exceptions which I was able to resolve once I removed the AjaxScriptManager control from the page. This, of course, breaks the ajax and rest functionality but that's a bug I don't know how to fix.

There is also a sample wizard that you can view or use the source to, that lives in the same folder:http://localhost/sitecore/shell/~/xaml/Sitecore.Shell.Xaml.WebControl.aspx

Alright, now that I've explained a bit about what Sheer UI is and how it's configured in Sitecore, my next articles will go through some Hello World examples.

Viewing all 178 articles
Browse latest View live