Using categories as tags in EPiServer
First of all, big thanks to Anders Hattestad and all his great contributions to the community! Some months ago he posted a great post on how to use categories in a better way. I´ve taken his code and refined it a bit.
I started with the gui first. The dropdown and the inputs needed some more styling to match EPiServer’s gui.
Then I realized that all page categories were saved to the property, so if you where retrieving the property by
CurrentPage["BlogPostTags"]
you would get all categories returned, not the ones just specified in the property. And you could not deselect any category on the page, just add new ones.
By default the property doesn´t hide the categories on the “Categories” tab. If you want to hide all categories just go to admin mode and change the visibility to false on the root category for the property.
namespace DV.Properties
{
using System;
using System.Collections.Generic;
using System.Web.UI;
using System.Web.UI.WebControls;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.PlugIn;
using EPiServer.Web.PropertyControls;
[Serializable]
[PageDefinitionTypePlugIn(
DisplayName = "Category selection - select or add",
Description = "Category selection - select or add")]
public class PropertySelectOrAddCategory : PropertyCategory
{
public override EPiServer.Core.IPropertyControl CreatePropertyControl()
{
return new PropertySelectOrAddCategoryControl();
}
}
public class AttachEvents : PlugInAttribute
{
public static void Start()
{
DataFactory.Instance.CreatingPage += new PageEventHandler(Instance_SavingPage);
DataFactory.Instance.SavingPage += new PageEventHandler(Instance_SavingPage);
}
static void Instance_SavingPage(object sender, EPiServer.PageEventArgs e)
{
CategoryList oldValue = e.Page.Category;
CategoryList newValue = new CategoryList();
bool customPropertyExists = false;
foreach (PropertyData prop in e.Page.Property)
{
if (prop.GetType() == typeof(PropertySelectOrAddCategory)
{
customPropertyExists = true;
foreach (int catId in oldValue)
{
Category thisCat = Category.Find(catId);
if (thisCat.Parent == null || thisCat.Parent.Name != prop.Name)
{
newValue.Add(catId);
}
}
foreach (int catId in (prop as PropertySelectOrAddCategory).Category)
{
newValue.Add(catId);
}
}
}
if (customPropertyExists)
{
e.Page.Property["PageCategory"].Value = newValue;
}
}
}
public class PropertySelectOrAddCategoryControl : PropertyStringControl
{
List<CheckBox> CheckBoxList = new List<CheckBox>();
TextBox oldItemsBox;
TextBox addNewBox;
public override void CreateEditControls()
{
#region Display controls
Panel outerDiv = new Panel();
outerDiv.Style.Add("position", "relative");
outerDiv.Style.Add("overflow", "visible");
this.Controls.Add(outerDiv);
addNewBox = new TextBox();
addNewBox.ID = this.Name + "NewText";
addNewBox.CssClass = "episize240";
addNewBox.Attributes.Add("autocomplete", "off");
outerDiv.Controls.Add(addNewBox);
Button addBtn = new Button();
addBtn.Text = "+";
addBtn.CssClass = "epismallbutton";
outerDiv.Controls.Add(addBtn);
Panel theDiv = new Panel();
theDiv.ID = this.Name + "Options";
theDiv.CssClass = "AutoCompleteWrapper";
outerDiv.Controls.Add(theDiv);
oldItemsBox = new TextBox();
oldItemsBox.Style.Add("display", "none");
oldItemsBox.ID = this.Name + "Old";
this.Controls.Add(oldItemsBox);
EPiServer.ClientScript.ScriptManager current = EPiServer.ClientScript.ScriptManager.Current;
if (current != null)
{
current.AddEventListener(addBtn, new EPiServer.ClientScript.Events.DisablePageLeaveEvent(EPiServer.ClientScript.EventType.Click));
}
#endregion
Category root = GetRoot();
Dictionary<string, CategoryItem> existing = new Dictionary<string, CategoryItem>();
Dictionary<string, CategoryItem> shown = new Dictionary<string, CategoryItem>();
List<Category> all = new List<Category>();
#region Find what to show
//First time
CategoryItem item = null;
foreach (EPiServer.DataAbstraction.Category cat in root.Categories)
{
item = new CategoryItem(cat);
if (!existing.ContainsKey(item.Key))
{
existing.Add(item.Key, item);
if (CurrentPage.Category.MemberOf(cat.ID))
{
shown.Add(item.Key, item);
}
}
}
//second time
if (this.Page.Request[oldItemsBox.UniqueID] != null)
{
string[] split = this.Page.Request[oldItemsBox.UniqueID].Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
foreach (string s in split)
{
item = new CategoryItem(s);
if (!existing.ContainsKey(item.Key))
{
existing.Add(item.Key, item);
shown.Add(item.Key, item);
}
else if (!shown.ContainsKey(item.Key))
{
shown.Add(item.Key, existing[item.Key]);
}
}
if (!string.IsNullOrEmpty(this.Page.Request[addNewBox.UniqueID]))
{
item = new CategoryItem(this.Page.Request[addNewBox.UniqueID]);
if (!existing.ContainsKey(item.Key))
{
existing.Add(item.Key, item);
shown.Add(item.Key, item);
}
else if (!shown.ContainsKey(item.Key))
{
shown.Add(item.Key, existing[item.Key]);
}
}
}
#endregion
foreach (CategoryItem s in shown.Values)
{
this.Controls.Add(new LiteralControl("<div>"));
CheckBox check = new CheckBox();
check.ID = s.Key;
check.Text = s.Name;
check.Checked = true;
this.Controls.Add(check);
this.Controls.Add(new LiteralControl("</div>"));
this.CheckBoxList.Add(check);
}
#region AutoComplete
ScriptManager.RegisterClientScriptBlock(this, typeof(PropertySelectOrAddCategory), "Startup" + this.Name, jsScript, false);
string dataArray = "";
string scriptString = "<script language=\"JavaScript\">";
foreach (CategoryItem sub in existing.Values)
{
if (dataArray != "")
dataArray += ",";
dataArray += "\"" + sub.Name + "\"";
}
scriptString += "\n new AutoComplete(" + System.Environment.NewLine +
" [" + dataArray + "]," + System.Environment.NewLine +
" document.getElementById('" + addNewBox.ClientID + "')," + System.Environment.NewLine +
" document.getElementById('" + theDiv.ClientID + "')," + System.Environment.NewLine +
" 25)";
scriptString += "</script>";
ScriptManager.RegisterStartupScript(this, typeof(PropertySelectOrAddCategory), "Startup" + this.Name, scriptString, false);
#endregion
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (oldItemsBox != null)
{
string saveVal = "";
foreach (CheckBox check in CheckBoxList)
{
if (saveVal != "")
{
saveVal += ";";
}
saveVal += check.Text;
}
oldItemsBox.Text = saveVal;
addNewBox.Text = "";
}
}
public override void ApplyEditChanges()
{
CategoryList result = new CategoryList();
Category root = GetRoot();
foreach (CheckBox box in CheckBoxList)
{
if (this.Page.Request[box.UniqueID] != null)
{
Category thisCat = root.FindChild(box.Text);
if (thisCat == null)
{
thisCat = EnsureCategoryExists(root, box.Text, box.Text, true);
}
result.Add(thisCat.ID);
}
}
SetValue(result);
}
public Category GetRoot()
{
Category root = Category.GetRoot();
return EnsureCategoryExists(root, this.Name, this.Name, false);
}
public Category EnsureCategoryExists(Category parent, string name, string description, bool selectable)
{
Category result = parent.FindChild(name);
if (result == null)
{
result = new Category(name, description);
result.Parent = parent;
result.Selectable = selectable;
result.Available = true;
result.Save();
}
return result;
}
public PropertySelectOrAddCategory PropertySelectOrAddCategory
{
get
{
return PropertyData as PropertySelectOrAddCategory;
}
}
string jsScript = @"
<style>
.AutoCompleteWrapper {
position:absolute;
visibility:hidden;
left:20px;
top:20px;
z-index:100;
border:1px solid #000;
background:#fff;
}
.AutoCompleteWrapper div {
padding:5px 10px;
cursor:hand;
cursor:pointer;
}
.AutoCompleteBackground {
background-color:#fff;
}
.AutoCompleteHighlight {
background-color:#ccc;
}
</style>
<script language=""javascript"">
function AutoCompleteDB() {
// set the initial values.
this.bEnd = false;
this.nCount = 0;
this.aStr = new Object;
}
AutoCompleteDB.prototype.add = function(str) {
// increment the count value.
this.nCount++;
// if at the end of the string, flag this node as an end point.
if (str == """")
this.bEnd = true;
else {
// otherwise, pull the first letter off the string
var letter = str.substring(0, 1);
var rest = str.substring(1, str.length);
// and either create a child node for it or reuse an old one.
if (!this.aStr[letter]) this.aStr[letter] = new AutoCompleteDB();
this.aStr[letter].add(rest);
}
}
AutoCompleteDB.prototype.getCount = function(str, bExact) {
// if end of search string, return number
if (str == """")
if (this.bEnd && bExact && (this.nCount == 1)) return 0;
else return this.nCount;
// otherwise, pull the first letter off the string
var letter = str.substring(0, 1);
var rest = str.substring(1, str.length);
// and look for case-insensitive matches
var nCount = 0;
var lLetter = letter.toLowerCase();
if (this.aStr[lLetter])
nCount += this.aStr[lLetter].getCount(rest, bExact && (letter == lLetter));
var uLetter = letter.toUpperCase();
if (this.aStr[uLetter])
nCount += this.aStr[uLetter].getCount(rest, bExact && (letter == uLetter));
return nCount;
}
AutoCompleteDB.prototype.getStrings = function(str1, str2, outStr) {
if (str1 == """") {
// add matching strings to the array
if (this.bEnd)
outStr.push(str2);
// get strings for each child node
for (var i in this.aStr)
this.aStr[i].getStrings(str1, str2 + i, outStr);
}
else {
// pull the first letter off the string
var letter = str1.substring(0, 1);
var rest = str1.substring(1, str1.length);
// and get the case-insensitive matches.
var lLetter = letter.toLowerCase();
if (this.aStr[lLetter])
this.aStr[lLetter].getStrings(rest, str2 + lLetter, outStr);
var uLetter = letter.toUpperCase();
if (this.aStr[uLetter])
this.aStr[uLetter].getStrings(rest, str2 + uLetter, outStr);
}
}
function AutoComplete(aStr, oText, oDiv, nMaxSize) {
// initialize member variables
this.oText = oText;
this.oDiv = oDiv;
this.nMaxSize = nMaxSize;
if (!oText)
return;
// preprocess the texts for fast access
this.db = new AutoCompleteDB();
var i, n = aStr.length;
for (i = 0; i < n; i++) {
this.db.add(aStr[i]);
}
// attach handlers to the text-box
oText.AutoComplete = this;
oText.onkeyup = AutoComplete.prototype.onTextChange;
oText.onblur = AutoComplete.prototype.onTextBlur;
}
AutoComplete.prototype.onTextBlur = function() {
this.AutoComplete.onblur();
}
AutoComplete.prototype.onblur = function() {
this.oDiv.style.visibility = ""hidden"";
}
AutoComplete.prototype.onTextChange = function() {
this.AutoComplete.onchange();
}
AutoComplete.prototype.onDivMouseDown = function() {
this.AutoComplete.oText.value = this.innerHTML;
}
AutoComplete.prototype.onDivMouseOver = function() {
this.className = ""AutoCompleteHighlight"";
}
AutoComplete.prototype.onDivMouseOut = function() {
this.className = ""AutoCompleteBackground"";
}
AutoComplete.prototype.onchange = function() {
var txt = this.oText.value;
// count the number of strings that match the text-box value
var nCount = this.db.getCount(txt, true);
// if a suitable number then show the popup-div
if ((this.nMaxSize == -1) || ((nCount < this.nMaxSize) && (nCount > 0))) {
// clear the popup-div.
while (this.oDiv.hasChildNodes())
this.oDiv.removeChild(this.oDiv.firstChild);
// get all the matching strings from the AutoCompleteDB
var aStr = new Array();
this.db.getStrings(txt, """", aStr);
// add each string to the popup-div
var i, n = aStr.length;
for (i = 0; i < n; i++) {
var oDiv = document.createElement('div');
this.oDiv.appendChild(oDiv);
oDiv.innerHTML = aStr[i];
oDiv.onmousedown = AutoComplete.prototype.onDivMouseDown;
oDiv.onmouseover = AutoComplete.prototype.onDivMouseOver;
oDiv.onmouseout = AutoComplete.prototype.onDivMouseOut;
oDiv.AutoComplete = this;
}
this.oDiv.style.visibility = ""visible"";
}
else // hide the popup-div
{
this.oDiv.innerHTML = """";
this.oDiv.style.visibility = ""hidden"";
}
}
</script>";
public class CategoryItem
{
public CategoryItem(string name)
{
Name = name;
}
public CategoryItem(EPiServer.DataAbstraction.Category cat)
{
Cat = cat;
Name = cat.Name;
}
public EPiServer.DataAbstraction.Category Cat;
public string Name;
string key;
public string Key
{
get
{
if (key == null)
{
key = Name.ToLower().Trim().Replace(" ", "").Replace(".", "");
}
return key;
}
}
}
}
}
Posted in EPiServer ● Tags ui, fixes, custom property

Comments
Great work, and cool that you have made the consept way better:)