Taxonomy Field Control for Sitefinity Front-End

June 30, 2014 Digital Experience, Sitefinity

Currently in Sitefinity we are using the built-in HierarchicalTaxonField and FlatTaxonField controls to show taxonomies both in the back-end and in the front-end, by switching the DisplayMode of the field controls. However, in the practice this proved not the most efficient way to display the taxonomies related to an item. Furthermore, most users want the ability to filter the items directly by clicking the taxonomy of a specific item.Consequently, we decided to create a field control that is lightweight and will serve to display all types of classifications – flat and hierarchical, built-in and custom, on the front-end. By default, the field control will render the taxa as hyperlink, clicking which, will construct the necessary query string, making the default widgets automatically filter the items by this taxa. There is an option to display them as a simple text, as well, the way the built-in TaxonFields display them.

Initializing the control

We create the control inheriting from the Telerik.Sitefinity.Web.UI.Fields.FieldControl and reusing its implementation.

/// <summary>
/// Custom field control used to display Taxa for Sitefinity front-end
/// </summary>
public class CustomTaxonomyFieldControl : FieldControl
{
  /// <summary>
  /// Initializes a new instance of the <see cref="CustomTaxonomyFieldControl" /> class.
  /// </summary>
  public CustomTaxonomyFieldControl()
   {
  }
  
  /// <summary>
  /// Getting and setting the value as a TrackedList of type Guid, used for taxa
  /// </summary>
   [TypeConverter(typeof(ObjectStringConverter))]
  public override object Value
   {
      get
      {
          return this.Taxa as TrackedList<Guid>;
      }
      set
      {
          this.Taxa = value as TrackedList<Guid>;
      }
  }
  
  /// <summary>
  /// The list of taxa passed to the field control
  /// </summary>
  protected TrackedList<Guid> Taxa { get; set; }
  
  /// <summary>
  ///  The property showing whether the Taxon items would be shown as hyperlinks or as text
  /// </summary>
  public bool HideLinks { get; set; }
  
  /// <summary>
  /// Main Url of the site - used to build the Taxon Url
  /// </summary>
  protected string MainUrl { get; set; }
  
  /// <summary>
  /// Used to prepare and bind the data to the field control's controls when initializng
  /// </summary>
  /// <param name="container">The container of the control</param>
  protected override void InitializeControls(GenericContainer container)
   {
        List<TaxonModel> data = PrepareData();
        BindData(data);        
  }
 
 /// <summary>
        /// We do not have any client side logic, so we use the inherited
        /// ScriptDescriptor type of the Telerik.Sitefinity.Web.UI.Fields.FieldControl
        /// </summary>
 protected override string ScriptDescriptorType
  {
      get
      {
          return typeof(FieldControl).FullName;
      }
 }
}

We get the received value and try to save it as the expected type – TrackedList<Guid>, if the passed value is not of this type and is invalid in this case, the Taxa property will be null. We will handle this value later in the binding section of the control. We declare a property – HideLinks, which will be used to indicate whether the Taxonomy items will be displayed as hyperlinks – anchor html tags in the markup or as a simple text – a span elements in this case.

In InitializeControls method we prepare the data and bind it to the control that will display it – a simple Repeater. We will handle in this method any exceptions that might be thrown while preparing and binding the data, as well. The Error handling is described below in the post, in Error handling section.

The control will not need any client side logic different by the default one, so we pass the inherited FieldControl type as the ScriptDescriptor one.

The field control is registered in the following way, if it is located in the SitefinityWebApp assembly, TaxonomyFieldControl  namespace:

<%@ Register Assembly="SitefinityWebApp" Namespace="SitefinityWebApp.TaxonomyFieldControl" TagPrefix="custom" %>

Use your own assembly and namespace names to register it.

Then use the TagPrefix and the field control class to declare it in the widget template where the taxa will be displayed:

<custom:CustomTaxonomyFieldControl  … />

Evaluate the field that contains the classification in the Value property:

<custom:CustomTaxonomyFieldControl runat="server" ID="CustomHierarchicalTaxonField"
          value='<%# Eval("Category") %>'  />

Preparing and Binding the data

/// <summary>
/// Preparing the Taxa to be bound. Convert the Taxa to DTO - TaxonModel model.
/// </summary>
/// <returns>The data to be bound.</returns>
private List<TaxonModel> PrepareData()
{
    if (this.Taxa != null && this.Taxa.Count > 0)
    {
        TaxonomyManager manager = new TaxonomyManager();
         
        IEnumerable<Taxon> taxaItems = manager.GetTaxa<Taxon>().Where(t => this.Taxa.Contains(t.Id));
 
        List<TaxonModel> data = new List<TaxonModel>();
 
        foreach (var taxon in taxaItems)
        {
            TaxonModel model = new TaxonModel();
            model.Title = taxon.Title;
 
            string taxonomyName = taxon.Taxonomy.Name;
 
            string taxonUrl = string.Empty;
 
            if (this.HideLinks == false) // HideLinks property default value
             {                       
                string url = GetMainUrl();
 
                string evaluatedResult = BuilTaxonUrl(taxon, taxonomyName);
                taxonUrl = string.Concat(url, evaluatedResult);
            }
 
            model.Url = taxonUrl;              
 
            data.Add(model);
        }
 
        this.TitleLabel.Text = string.Format("{0}:", taxaItems.First().Taxonomy.Name);
        return data;
    }
    else
    {
        this.Visible = false;
        return null;
    }
}
 
/// <summary>
/// Bind the TaxonModel items to the Repeater, which will display them.
/// </summary>
/// <param name="data">The TaxonModel list.</param>
private void BindData(List<TaxonModel> data)
{
    this.RepeaterControl.DataSource = data;
    this.RepeaterControl.DataBind();
}

The method uses the property Taxa in which we saved the passed value to the field control. We check whether it is not null and it contains any taxa to be evaluated. If not, we hide the control, since it has nothing to display. We then used the Id of the taxa to get it. We get all taxa items using the Ids in the Tracked<Guid> list. We will be using a very simple model to show the items, only with the necessary properties:

/// <summary>
/// Model representing Telerik.Sitefinity.Taxonomies.Model.Taxon
/// </summary>
public class TaxonModel
{
    /// <summary>
    /// Taxon Title
    /// </summary>
    public string Title { get; set; }
 
    /// <summary>
    /// Taxon filter Url
    /// </summary>
    public string Url { get; set; }
}

The construct Url part of the field control logic you can find in this section.

We add all items to a List, which will be then bound to a Repeater to display them. We set the field name of the control, using the first item, since we have checked that it exists and we know that all items will of the same classification. Then, the data list is bound.

Construct Url

First, the main page Url should be constructed. We will use a method that will set it once, and only returned it for each Taxa:

/// <summary>
/// Get the page Url using the System.Web.SiteMapProvider
/// </summary>
/// <returns>The page Url</returns>
protected string GetMainUrl()
{
    if (string.IsNullOrWhiteSpace(MainUrl))
    {
        SiteMapProvider siteMap = SiteMapBase.GetCurrentProvider();
        string url = string.Empty;
 
        // The CurrentNode will be null if we are in Page's Revision History
        if (siteMap.CurrentNode != null)
        {
            url = siteMap.CurrentNode.Url;
 
            if (VirtualPathUtility.IsAppRelative(url))
            {
                // Get absolute Url
                url = VirtualPathUtility.ToAbsolute(url);
            }
 
        }
 
        this.MainUrl = url;
    }
 
    return this.MainUrl;
}

We use the SiteMap provider to get the Url. Then, the taxonUrl is generated using the Telerik.Sitefinity.Web.UrlEvaluation.TaxonomyEvaluator. There is a slight difference of the generated Url for Flat and Hierarchical taxonomies, so I have split this up:

/// <summary>
        /// Build the Taxon Url that will be used to filter the Widget displaying the content items
        /// </summary>
        /// <param name="taxon">The Taxon used</param>
        /// <param name="taxonomyName">The Taxon, Taxonomy name</param>
        /// <returns>The Filter Url</returns>
        private string BuilTaxonUrl(ITaxon taxon, string taxonomyName)
        {
            TaxonomyEvaluator evaluator = new TaxonomyEvaluator();
 
            TaxonBuildOptions taxonBuildOptions = TaxonBuildOptions.None;
            string evaluatedResult = string.Empty;
 
            if (taxon is HierarchicalTaxon)
            {
                taxonBuildOptions = TaxonBuildOptions.Hierarchical;
                HierarchicalTaxon hierarchicalTaxon = taxon as HierarchicalTaxon;
 
                evaluatedResult = evaluator.BuildUrl(taxonomyName, hierarchicalTaxon.FullUrl,
               taxon.Taxonomy.TaxonName, taxonBuildOptions, this.GetUrlEvaluationMode(), null);
            }
            else
            {
                taxonBuildOptions = TaxonBuildOptions.Flat;
 
                evaluatedResult = evaluator.BuildUrl(taxonomyName, taxon.UrlName.Value,
                  taxonomyName, taxonBuildOptions, this.GetUrlEvaluationMode(), null);
            }
 
            return evaluatedResult;
        }

At the end, everything is assembled together:

if (this.HideLinks == false) // HideLinks property default value
{                       
        string url = GetMainUrl();
 
        string evaluatedResult = BuilTaxonUrl(taxon, taxonomyName);
        taxonUrl = string.Concat(url, evaluatedResult);
}
 
 model.Url = taxonUrl;

 

Displaying the Taxa items

Here is shown the template of the field control:

<%@ Control %>
 
<div>
<b><asp:Label ID="fieldTitle" runat="server"></asp:Label></b>
 
<asp:Repeater ID="TaxaRepeater" runat="server">
     <ItemTemplate>          
         <a runat="server" visible='<%# (DataBinder.Eval(Container.DataItem, "Url").Equals(String.Empty) = False)%>'
              href='<%#DataBinder.Eval (Container.DataItem,"Url")%>'>
             <%#DataBinder.Eval(Container.DataItem, "Title")%></a>   
         
         <span runat="server" visible='<%# (DataBinder.Eval(Container.DataItem,"Url").Equals(String.Empty)) %>'>
             <%#DataBinder.Eval(Container.DataItem, "Title")%></span>
    </ItemTemplate>
    <SeparatorTemplate>
        <span>, </span>
    </SeparatorTemplate>
</asp:Repeater>
</div>

In the label is shown the name of the classification, for instance ‘Tags’ or ‘Categories’. The repeater uses Item and Separator templates. In the item template are used a span or and anchor HTML elements and either the one or the other is shown depending on the value of the item Url, we have set its default value to be string.Empty, which will make the anchor disappear, with Visible set to false.In the SeparatorTemplate we state the items separator, in this case just a space, followed by a comma: ‘ ,’.

 

Error handling

The error handling is very simple and efficient in our field control. We have encompassed the whole custom logic in a try/catch statement:

/// <summary>
        /// Used to prepare and bind the data to the field control's controls when initializng
        /// </summary>
        /// <param name="container">The container of the control</param>
        protected override void InitializeControls(GenericContainer container)
        {
            try
            {
                if (this.GetIndexRenderMode() == Telerik.Sitefinity.Web.UI.IndexRenderModes.Normal)
                {
                    List<TaxonModel> data = PrepareData();
                    if (data != null)
                    {
                      BindData(data);                       
                    }
                }
            }
            catch (Exception ex)
            {
                // If in Backend, throw the exception again,
                // it will be shown in the widget placeholder
                if (this.IsBackend())
                {
                    throw ex;
                }
                else
                {
                    // Hide the field control
                    this.Visible = false;
                    // Write in error log
                    Log.Write(ex, ConfigurationPolicy.ErrorLog);
                }
            }
        }

We check whether the control is being rendered by the Search Engine. If not, the control will be in the IndexRenderModes.Normal and we continue. We are using the SiteMap to get the current node, which will be null if in Revision History, so we validate this, as well. See the Construct Url section regarding this.

When an exception is thrown, we check whether we are in back-end or front-end mode and act accordingly. If we are in the Back-end, editing a page with a widget containing the field control and it throws an exception, the exception is being caught and re-thrown, and will be shown its message in the layout control, holding the widget. However, if we are in the Front-end, we do not want our users to see the error, so we just hide the field control and write the exception in the Error log, using the helper static class Telerik.Sitefinity.Abstractions.Log:

Log.Write(ex, ConfigurationPolicy.ErrorLog);

State in which log to write using the ConfigurationPolicy enumeration, we choose the error.log file in this case. You can use different overloads of the Write method and customize the exception message shown in the log:

Log.Write(string.Format("Exception thrown in {0} : {1}", typeof (CustomTaxonomyFieldControl).Name, ex),
                       ConfigurationPolicy.ErrorLog);

Here is a short video of the field control functionality:

Short video of explicitly throwing an error and showing the Error handling:

This part of the page will be loaded later.

  

You can download the full source from here:

TaxonomyFieldControl

Nikola Zagorchev

Nikola Zagorchev is a Tech Support Engineer at Telerik. He joined the Sitefinity Support team in March 2014.