Expression Evaluation Time: ms / Round trip: ms
How to implement a tag cloud web part
23. Jul
2013
by Markus Lindenmann Category:
Technical
0

Throughout this blog post, a Tag Cloud Web Part is implemted. Following MatchPoint's fundamental principle, in a very generic manner: supporting different sources and a user defined appearance. Furthermore, the tag cloud is not just summarizing and visually representing tags, but also working like a simple refiner; i.e. tags are selectable and used to generate a filter condition.

Introduction

Throughout this blog post, a Tag Cloud Web Part is implemted. Following MatchPoint's fundamental principle, in a very generic manner: supporting different sources and a user defined appearance. Furthermore, the tag cloud is not just summarizing and visually representing tags, but also working like a simple refiner; i.e. tags are selectable and used to generate a filter condition.

This blog post provides an implementation of a consumer / producer web part, described in a previous blog post (see How to implement a consumer / producer webpart).

Implementation

Overview

The Tag Cloud Web Part implementation consists of multiple components:

  • A C# file, describing the HTML generation and server sided code
  • A JavaScript code behind file, as mediator between server and front end and handling click events
  • A CSS file, to define some basic layout

Configuration

The Tag Cloud Web Part configuration is specified in the class TagCloudWebPartConfiguration, which implements the interface Colygon.MatchPoint.Core.WebParts.IWebPartConfiguration and is a member of the namespace Colygon.MatchPoint.Samples.WebParts. It specifies the web part's properties shown in the 'MatchPoint Configuration Editor'.

Each of the following public fields describes a setting in the web part's configuration, later rendered in the 'MatchPoint Configuration Editor'. The Colygon.MatchPoint.Core.Administration.ConfigurationEditor.MemberDescriptorAttribute is used to describe these settings and to define the appearance in the 'MatchPoint Configuration Editor'; the first constructor argument is a textual description, and the (optional) Boolean flag describes whether the setting is mandatory or not. For completeness it is to be mentioned that there are further optional constructor arguments, e.g. to specify a friendly name, etc.; However, they are not required in this example and hence not described further.

[Serializable]
[ClassDescriptor(Category = "Web Parts")]
public class TagCloudWebPartConfiguration : IWebPartConfiguration
{
  [MemberDescriptor("Specifies the name, used in the connection framework.")]
  public string Name;

  [MemberDescriptor("Specifies the data source of the tag cloud. Note, that this is ignored if DataSourceExpression is specified.")]
  public BaseDataProvider DataProvider;

  [MemberDescriptor("Specifies the data source of the tag cloud.")]
  public ExpressionString DataSourceExpression;

  // note (ml): This requires the internal column name, since the name resolution is not accessible!
  [MemberDescriptor("Specifies a column which should be used to generate the Tag cloud. If not specified the MatchPoint Tags are used.")]
  public string ColumnName;

  [CustomEditor(typeof(CodeEditor))]
  [CodeEditorBehavior(SyntaxHighlighting.XmlHtml)]
  [MemberDescriptor("Specifies a custom pattern, used to render a tag in the Tag cloud. Use '{Clickable}', '{Size}', '{Name}', {Index} and '{NrOccurrences}' to build a custom pattern.")]
  [XmlCData]
  public PatternString RenderingTemplate =
    "<span {Clickable} style=\"font-size:{0.5+3.5*Size}em;\" class=\"tagCloud-TagElement\">{Name.EncodeHtml()}</span>";

  [MemberDescriptor("Specifies, how many tags should be included into the tag cloud.")]
  public int TagLimit;

  public virtual WebPart CreateWebPart()
  {
    return new TagCloudWebPart();
  }
}

There are some parts in this configuration, which might seem confusing at first sight:

  • There are two possible sources of information specified: DataProvider and DataSourceExpression. We want to support both and hence have to handle the cases, where either one, both, or none is specified. This implies some prioritization and error handling later in the code, since we cannot provide mutual exclusion on configuration level.
  • The ColumnName requests the internal column name; this is necessary, since we cannot (yet) access the mapping between all data providers internal and display names via the API.
  • The RenderingTemplate member is decorated with some attributes:
    • CustomEditor: declares, that we want a customized text box; typeof(CodeEditor) specifies, that we want it to behave like a code editor. Colygon.MatchPoint.Core.Administration.Controls.CustomEditorAttribute
    • CodeEditorBehavior: this is related to the previous CustomEditor/CodeEditor attribute and specifies, that we want syntax highlighting for this field. Colygon.MatchPoint.Core.Administration.Controls.CodeEditorBehaviorAttribute
    • XmlCData: Specifies, that this field is serialized into a CDATA block and that some encoding might be required. Colygon.MatchPoint.Core.Administration.XmlSerialization.XmlCData

After deploying, the configuration can be selected under 'Create' in the 'Manage MatchPoint Configurations' view (Ctrl-M). It is then rendered in the 'MatchPoint Configuration Editor', as depicted in Figure 1 below. The settings specified in the DetailViewWebPartConfiguration class are rendered in a form, according to the fields types, their names and the assigned MemberDescriptor attributes.

Figure 1: Tag Cloud Web Part Configuration, rendered in 'MatchPoint Configuration Editor'

The web part

The web part's logic is implemented in an extension of Colygon.MatchPoint.Core.WebParts.BaseWebPart, where the previously implemented configuration is used as generic type argument.

public class TagCloudWebPart
                        : BaseWebPart<TagCloudWebPartConfiguration>
{
}

In a next step, we override OnInit(EventArgs e) and RenderContents(HtmlTextWriter writer), to be able to register our new web part as consumer and producer, and to render the Tag Cloud Web Part.

protected override void OnInit(EventArgs e)
{
  base.OnInit(e);
  try
  {
    if (Configuration.Name != null)
    {
      ConnectionManager.RegisterDataSource(this, Configuration.Name,
                                               typeof (BaseCondition));
      JavaScriptBehaviorManager.RegisterBehavior(this,
                                            "Samples.TagCloudWebPart");
    }
    if (Configuration.DataSourceExpression != null)
    {
      DependencyCollection dc = new DependencyCollection();
      dc.AddFromExpression(Configuration.DataSourceExpression);
      if (Configuration.ColumnName != null)
      {
        dc[0].Add(Configuration.ColumnName);
      }
      ConnectionManager.RegisterConsumer(this, dc);
    }
  }
  catch (Exception ex)
  {
    HandleError(ex);
  }
}

protected override void RenderContents(HtmlTextWriter writer)
{
  try
  {
    writer.Write(GetHtml());
  }
  catch (Exception ex)
  {
    HandleError(ex);
  }
}

public string GetHtml()
{
  return "";
}

While implementing RenderContents, we notice that this is the first location, where we might need some more logic – let's postpone that for a second, and implement a dummy method GetHtml(). We will instead have a look, on the communication between the client and the server. First of all, we know that we have to update our connection data in the 'MatchPoint Connection Framework', whenever the user selects a tag. Furthermore, since we might depend on a changing source, we need to be able to update our tag cloud, when the underlying information changes. Hence we need the following two methods to be callable from the client:

[AjaxMethod]
public void UpdateSelectedTags(string key) {}

[AjaxMethod]
public string GetHtml()
{
  return "";
}

To be able to update the selected tags, we have to store the user's selection somehow. We do this using a global list, which we synchronize between client and server by annotating the declaration with the Colygon.MatchPoint.Core.Ajax.JavaScriptBehaviorVariableAttribute. The sync-mode is set to be included in the callback.

[JavaScriptBehaviorVariable(SyncMode.IncludeInCallback)]
protected List<string> SelectedKeys;

Since we want to support 'MatchPoint Tags' as well as strings, we need an abstraction layer between our logic, the view and the source. This is achieved by implementing an inner class representing tags. This class stores a key, a display name and an index, for each tag:

  • display name: the tag's name to be shown to the user (not necessarily unique)
  • key: the tag identifier (is assumed to be unique)
  • index: represents the ranking, where zero is the most important tag

For later use, this class needs to implement IEquatable. Since the key is assumed to be unique, it is sufficient to compare the key!

private class TagCloudItem: IEquatable<TagCloudItem>
{
  internal readonly string Key;
  private readonly string displayName;
  internal float Size;
  internal int NrOccurrences;
  internal int Index;

  internal string DisplayName
  {
    get { return displayName ?? Key; }
  }

  internal TagCloudItem(string key, string displayName)
  {
    if (String.IsNullOrEmpty(key))
    {
      throw new ArgumentNullException("key");
    }
    this.Key = key;
    this.displayName = displayName;
  }

  public bool Equals(TagCloudItem other)
  {
    return other != null && other.Key == Key;
  }

  public override int GetHashCode()
  {
    return Key.GetHashCode();
  }

  public override bool Equals(object obj)
  {
    return Equals(obj as TagCloudItem);
  }
}

Now we have all preliminaries, to implement the postponed methods GetHtml() and UpdateSelectedTags(string key).

UpdateSelectedTags is toggling the selection for a provided tag key and updating the connection data according to the (new) selection. Afterwards, the connection data is set to a condition, that can be used to filter the source; this implies, that the condition is depending on the source type, initially providing the tags. This decision process is extracted to the private method GetCondition(), which is either returning a Colygon.MathPoint.Tagging.TagCondition for 'MatchPoint Tags', or an AndGroupCondition of FieldConditions in all other cases. Since we declared our consumer to return a BaseCondition, it is to be mentioned, that all three conditions, the TagCondition, the AndGroupCondition, and the FieldCondition, are extending BaseCondition. All these condition classes, except TagCondition, are members of the namespace Colygon.MatchPoint.Core.DataProviders.Conditions.

[AjaxMethod]
public void UpdateSelectedTags(string key)
{
  if (Configuration.Name == null) return;
  if (SelectedKeys == null)
  {
    SelectedKeys = new List<string>();
  }

  if (!SelectedKeys.Remove(key))
  {
    SelectedKeys.Add(key);
  }

  ConnectionManager.SetData(Configuration.Name,
                          SelectedKeys.Count == 0
                                  ? null
                                  : GetCondition());
}

private BaseCondition GetCondition()
{
  if (Configuration.ColumnName == null)
  {
    return new TagCondition(SelectedKeys.Select(
           i =>
           Tag.Load(i, TaggingClientContext.Current)).ToArray());
  }
  else
  {
    return new AndGroupCondition(
      SelectedKeys.Select(
        t =>
        new FieldCondition(Configuration.ColumnName,
          FieldType.AutoDetect, ComparisonOperator.EqualTo, t))
          .Cast<BaseCondition>().ToArray());
  }
}

GetHtml() is supposed to render our tag cloud according to the user defined pattern. Let's assume, we already have the tags, just to achieve some abstraction. We then just iterate over the tags and insert them into the user defined pattern. Since RenderingTemplate is a PatternString, this can be achieved easily: we define a variable scope defining the values and evaluate the PatternString using this scope. Then we concatenate all the resulting strings and return them. As a small modification, we render the keys in a hidden input HTML tag and assign them client sided.

[AjaxMethod]
public string GetHtml()
{
  List<string> keys = new List<string>();
  StringBuilder html = new StringBuilder();
  MPScope scope = MPScope.CreateDefaultScope();
  foreach (TagCloudItem tci in ExtractTags(GetRows()))
  {
    scope.Variables["Clickable"] =
                    String.Format("data-clickable=\"{0}\"", tci.Key);
    scope.Variables["Size"] = tci.Size;
    scope.Variables["NrOccurences"] = tci.NrOccurrences;
    scope.Variables["Name"] = tci.DisplayName;
    scope.Variables["Index"] = tci.Index;
    html.Append(Configuration.RenderingTemplate.Evaluate(scope));
  }
  return html.ToString();
}

The result of this rendering process (using the default rendering pattern) could look as exemplary shown in Figure 2.

Figure 2: Tag Cloud Web Part rendering

Finally, we have to implement the actual tag extraction process. As already seen in the previous code snippet, ExtractTags is executed on the output of GetRows. GetRows is therefore handling the source distinction: ExpressionString or DataProvider.

ExtractKeys is then deciding, whether to retrieve tags from the MatchPoint-Tags-column, or from a specified column – it is then counting and sorting the several tags, to finally return the most significant tags.

private IEnumerable<object> GetRows()
{
  // Omitted for readability. Short logic description:
  // As mentioned in the configuration, a prioritization is required:
  // 1) _ExpressionString_ is preferred, if both sources are specified
  // 2) If only an _ExpressionString_ is provided: try FAST refiner.

  // return rows.
}

private TagCloudItem[] ExtractTags(IEnumerable<object> rows)
{
  // Omitted for readability. Short logic description:
  // foreach row : get tags
  // foreach tag : create TagCloudItem and count occurrences
  // sort ascending by occurrence
  // take top <Configuration.TagLimit> elements
  // calculate relative size and normalize maximum to 1
  // sort remaining elements by their DisplayName
  // return elements.
}

JavaScript code behind

As described previously in the web part implementation section, the server provides two callback methods: UpdateSelectedTags() and GetHtml(). Now, on the client-side, we have to use these two methods, to send the user actions to the server sided web part control. The main task is, to bind the event handlers to the tag elements. We previously introduced the "{Clickable}" variable in the rendering pattern. We have to find these elements again and bind a click event handler to it as well as copying the identifier key to it. The ClickSelectDelegate_ is handling the visual feedback for the user and sending the information to the server; the server updates the connection data and finally the client notifies all consumers of this information.

$$.Namespace("Samples").TagCloudWebPart = function ()
{
  this.Setup = function()
  {
    this.BindEventHandlers();
  };

  this.BindEventHandlers = function()
  {
    var me = this;
    $(this.Control).find("[data-clickable]").each(function (i)
    {
      $(this).click($$.Delegate.Create(me, me.Click_SelectDelegate));
    });
  };

  this.Click_SelectDelegate = function(e)
  {
    var target = $(e.currentTarget);

    target.toggleClass("tagCloud-ClickableTagSelected");

    var tag = target.closest("[data-tagKey]").attr("data-tagKey");
    this.Callback.UpdateSelectedTags(tag,
        $$.Delegate.Create(this, this.UpdateConnectionData_Callback));
  };

  this.UpdateConnectionData_Callback = function()
  {
    MP.ConnectionManager.NotifyConsumers(this);
  };

  this.Refresh = function()
  {
    if (this.Control)
    {
      $$.ProgressIndicator.Show(this.Control);
      this.Callback.GetHtml(
                     $$.Delegate.Create(this, this.GetHtml_Callback));
    }
  };

  this.GetHtml_Callback = function(res)
  {
    $$.ProgressIndicator.Hide(this.Control);
    $(this.Control).html(res.Value);
    this.BindEventHandlers();
  };
}

CSS

As seen in the JavaScript code behind, some CSS classes were used. They are required only, to give the user a visual feedback about the current selection (e.g. .tagCloud-ClickableTagSelected) and to take some complexity from the custom rendering pattern.

Source code

All three files (C#, JavaScript, CSS) are available in this ZIP archive.

Comments
ABOUT

This blog is about technical and non-technical aspects of the product MatchPoint and other SharePoint topics.

If you would like to post an article or if you have an idea for a post, please contact us.

ARCHIVE
COMMENTS
Matthias Weibel
09.04.2018 01:12
Link is updated and works now. | Goto Post
Dhanabalan
09.04.2018 12:21
Link doesn't work. Could anyone explain what does... | Goto Post
matthiaszbrun
14.03.2018 02:05
Hi Markus
We I use the config for SiteCollectionSe... | Goto Post
Reto Jeger
04.10.2017 09:15
Hello Reiner,
Thanks for pointing out the missing ... | Goto Post
rganser
29.09.2017 09:56
Hi, I downloaded the ZIP-file for MatchPoint Versi... | Goto Post