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:
Nikola Zagorchev
Nikola Zagorchev is a Tech Support Engineer at Telerik. He joined the Sitefinity Support team in March 2014.