Tuesday, May 5, 2015

Sitecore: Custom Standard Value Tokens

Overview

In Sitecore, all developers are familar with the concept of standard values.  Standard values are default values and behaviors that can be set for content items that derive from data templates.  You can define the default values for fields or you can assign behaviors such as insert options.  I will assume that the audience who reads this is already familiar with this concept and knows how to create and work with standard values.

Standard Value Token Variables

Standard values can be defined using static values such as a number or a string or they can be token variables that will be replaced by computed values at item creation time.  The default installation of Sitecore already includes a set of tokens that satisfy most applications and purposes. 
  • $name: The item name
  • $id: The item ID
  • $parentid: The item ID of the parent item
  • $parentname: The item name of the parent item
  • $date: The system date
  • $time: The system time
  • $now: The combination of system date and time
Tokens are strings that start with the "$" symbol.  When a content item is created in the content tree, the pipeline that gets invoked is:



Here, you can see that there are processors to perform basic checks and determine if token variables are detected before actually performing any substitutions.  But what if we want to define custom tokens?  Where do we inject that logic in this process?  Well, there are two possibilities.  One approach would be to inject a processor at the end of the pipeline where more custom tokens are defined and then perform additional substitutions on the new custom tokens, hence adding a "patch" to the way the pipeline works.  A better, leaner approach is to actually modify the "core" of it all to include the custom tokens and to perform substitutions as if everything came out of the box.

Taking Apart The Pipeline




First, let's use a decompiler to inspect what's happening in the expandInitialFieldValue pipeline.  In the ReplaceVariables processor, you will see that there is a call to another class that does all the work.  This class is define in the <settings> section of web.config.



Lets look at this class and see what is does and whether or not we can subclass it.  Knowing how flexible Sitecore is, the asnwer is probably yes.  Again, lets use a decompiler.  Of particular interest are the following three methods.







The execution order is Replace > ReplaceValues > ReplaceWithDefault with Replace being a virtual method while the others are not.  Fortunately for us, this means we can easily override the combined logic with a custom subclass of our own. 



In our custom class, we have to override the Replace method with the same or similar code.  Then we need two local private versions of ReplaceValues and ReplaceWithDefault.  We can use same or similar code for ReplaceWithDefault but the ReplaceValues method is where you would define your custom tokens and also tell Sitecore what to do with it.  For example, let's say you want to replace the custom "$test" token with the string "hello" this would be the resulting code.



That is all there is to define custom token variables for Sitecore standard values.  All the work is done in the ReplaceValues method. 

Custom Tokens For Date Fields

Now let's explore some of the more complex field types besides just the single-line string field.  A Sitecore date field renders as a datepicker in the content admin.  If you define the standard value for a date field,  you can pick a default date and all items will inherit this default date.  Did you know you can also replace the standard value in the date field by typing in a token name instead of using the datepicker?



If you use any of the default tokens such as "$name" or "$id" the resulting value of the field would be an empty string because the date field looks for a valid ISO-formatted datetime string such as "20150329T000000".  The only way to have a string in this format rendered is by defining a custom token with custom logic. 

Let's say the scenario is that we want the date field value to reflect the item name, we could define the "$itemnameasdate" token to be replaced like this.



Basically, if the item name is in a date format that is expected, then we convert the value to an ISO-formatted datetime string and replace the token with this string, otherwise the field will have no value.

Please keep in mind that all tokens should start with a unique string or substitutions will be invalid.  For example, if the item name is "testitem", the "$name" token will be replaced with "testitem".  If a custom token is name "$nameasdate", the substituted valued would be "testitemasdate" because a straight string.Replace method is called and your custom token replace logic will never be invoked.

Custom Tokens For Integer Fields

Integer fields are very tricky when attempting to set standard values.  You can set static integer values as the default values, no problem there.  Since token variables are string values, does this mean we are out of luck?  Kind of.  If we define a field as type Integer, we are only allowed to use integers in the standard value for that field.  This would mean we are out of luck when attempting to put down "$name" as the token to be replaced.





What can we do?  Well, behind the scenes all field values are stored as strings.  The UI is the only part that performs validations and barks at you when attempting to enter a non-integer value in the field.  So one unconventional practice is to first define the field as a string field, enter the token as the standard value, and then change the field type back to integer.  The token sticks!  Now you have a string token in an integer field as the standard value!


Next, we have to add the logic to perform the token replacement.  Let's take for example a data template that defines what a year is and we try to fill in the year integer field with the value of the item name.  If the item name can be interpreted as an integer, then it is a valid year, otherwise leave blank.  I am sure there can be more validations performed on this value to determine if the number is a year but let's keep it simple for now.  We can use this snippet of code.



There are no guarantees for this kind of workaround but at least it works as of 7.x now.  In the future if Sitecore decides to validate token names in standard values, then this code will not work but nothing will break and existing data will no be affected.  Since it is a standard value, only new items will be affected and there will be no default values set.

Final Words

Please keep in mind that if the class definition changes in future revisions of Sitecore and the Sitecore.Kernel assembly, the code in your subclass probably has to be updated to reflect the new base class code.  The code in these classes rarely change so it is not a concern but please just be aware of the possibility.




Wednesday, January 21, 2015

Sitecore Upgrade Headaches: Analytics

We once upgraded a Sitecore site from 6.5 to 6.6 and there was an error thrown related to Analytics configurations.  In the Sitecore.Analytics.Config file, please find this entry:


<setting name="Analytics.PerformLookup" value="true" /> 

 This would perform a GeoIP lookup of the current user.  This service also requires a paid subscription to a GeoIP lookup provider.

The url of the provider is here:

<setting name="MaxMind.Url" value="http://sitecore1.maxmind.com/app/sc?i={0}&amp;l={1}" />

These errors are usually nested deep inside the log files and will not cause site downtime but fixing this stops unnecessary log entries.

Thursday, October 23, 2014

Google Maps API: How To Add Multiple Location Markers

Overview

If you ever had the opportunity to work with the Google Maps Javascript API v3.0 you would see that everything is very straightforward.  First, you initiate a map, specify the center point, specify a zoom level, or even drop a marker(pin), on a specific address.  All this is basic stuff and used by most websites that require mapping functionalities.  Google Maps likes to deal with latitude and longitude combinations because that is the underlying location standard.  It even provides a tool to geocode natural addresses and convert them into lat/lang combos.

Problem

Even though Google doesn't mind if you pass in lat/lang combos or natural street addresses, lat/lang combos would offer optimal performance because there is no data manipulation or conversion needed.  But that is not always the case in real life.  Sometimes we only have natural addresses that we can use.  No worries, because we can call the geocode function of the API to get back a lat/lang object.  The function to geocode an address and drop a marker on the map can be as simple as this:




But what if you have multiple addresses and need to have markers on the same map?  If you know the lat/lang combos, it is very easy.  Just pass in an array of lat/lang combos like this:



Then iterate though the array and create each marker one by one and associate it with the map.  Now, what if we were only given addresses to work with?  That means we would have to geocode every single one of the addressed to get the associated lat/lang combo.  This approach has two issues.

1) Geocoding is an asynchronous call and does not normally process address by address in the order you expect so going through a simple for loop to continuously call the geocoder will not yield expected results.  Unless you zoom out on the map all the way to the global level, maybe you will see desired results as you see the pins drop one at a time.  But that is not usually desired, you want the map to be centered over the dropped pins and zoomed in as much as possible to fit all the pins on the map so unless you know when the last address in the array has been geocoded, you cannot make the calculations required.

2) Geocoding takes a lot of resources from Google and they frown upon multiple calls happening too frequently so calls will start to fail after a few attempts.  There is no specific number of milliseconds to wait so its a matter of trial and error until the error message is not thrown anymore.  They also expect that service providers of mapping data to provide lat/lang data and not just natural addresses so geocoding addresses really is a last resort.


Solution

How do we get around the issue of the asynchronous calls?  It is quite simple.  Just make sure the next geocode request starts after the success of the current one.  To do this, we need to perform the geocodes recursively and checking for success.  First, define a method that takes in an array of string addresses and an object of type LatLngBounds.



...



You should also define a global array of Markers as a place to store all the markers that have been created for addresses that have been processed already.

Next, we perform the geocoding for an address as usual but upon success we need to store the marker in the array.




Then we extend the LatLngBounds object to include the current lat/lang position that was just calculated.




At the end of all this, you need to pop the current address out of the array and call the function again.





So far this appears to work but then we still run into the issue of performing too many geocoding requests consecutively.  A simple trick can be used to get around this, pause a little in between calls.  You can have this call at the beginning of the method.





This simple function is defined as:




At the end of all the geocode requests we can make use of all the lat/lang positions that have been added to the LatLngBounds object.



Fortunately, the map object has a fitBounds function built in that performs all the calculations need to determine the center point of all the lat/lang locations and the appropriate zoom level based on the canvas size of the map.  The calculated center point location and zoom level will also override anything you set manually when initializing the map.

Conclusion

Again, you should not be too reliant on the geocoding features of the API.  Even if you pay a monthly fee, you still cannot make consecutive calls too frequently and there is still a limit on the number of requests per IP address per day, just more than free users.  Try to use lat/lang combos whenever possible.  If the data provider does not provide this data, then maybe its time to find another provider.