вторник, 23 ноября 2010 г.

Пишем свое облако тегов

Для группировки элементов списка часто применяют иерархическую структуру. Примером такой структуры может быть файловая система. В ней есть директории, внутри директорий находятся файлы и другие директории. Соответственно в этих директориях свои файлы и директории. Таких нехитрым образом можно к примеру группировать какие то документы. Например в одной директории находятся документы связанные с работой, в другой – персональные документы. Дальше, например в директории с рабочими документами есть свои директории, к примеру директория с финансовыми документами, директория со справками и директория с черновиками. Казалось бы – все есстественно, и разве как-то может быть иначе? Оказывается может. Другой вариант, который я хочу рассмотреть в данной статье – использование меток (тегов) для описания принадлежности нашего документа какой то категории. Например, рабочий документ содержащий какую-то справку – можно пометить двумя метками: «работа», и «справка». В этом случае это будет равосильно тому, как если бы данный документ находился в директории "справки" который в свою очередь – в "работа".

Очевидных преимуществ подхода с метками по сравнению с иерархической моделью достаточно – к примеру для того что бы один и тот же документ «находился в разных директориях» достаточно добавить ему соответствующую метку. В случае с иерархической моделью пришлось бы копировать документ в требуемую директорию. А в случае когда потребовалась бы модификация документа – пришлось бы редактировать как оригинал так и все его копии. Подход использующий метки позволяет абстрагироваться от того, где и как хранится документ и избавляет от необходимости создавать копии для различных «директорий». Документы храниться не только непосредственно в файловой системе, а в любом удобном для этого месте: FTP, базы данных, удаленный сервер. Еще одним преимуществом использования меток явлется возможность визуального отображения статистики - каких документов больше, а каких меньше в заданной «директории». Чем больше документов – тем большим шрифтом отображается метка, и наоборот. Этот прием думаю всем знаком – облако тегов – популярный элемент в современных страницах.

Давайте посмотрим на реализацию данного подхода изнутри с технической точки зрения.

Облако тегов изнутри

Элемент управления предназаначен для отображения меток. Сам по себе он представляет собой простой слой (div), в котором находятся ссылочные метки. У каждой метки – соответсвенно свой текст и свой размер шрифта. По нажатию на метку осуществляется вызов серверного обработчика, который инициирует событие TagClicked. Событие помимо всего прочего содержит информацию о том, на какую метку кликнул пользователь.

Ниже представлена ASP разметка элемента управления.

<div style="width:100%; height:100%;">
<asp:Repeater id="cloudCtrl" runat="server">
<ItemTemplate>
<asp:LinkButton OnClick="OnClick" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "Key") %>'
Font-Size='<%# DataBinder.Eval(Container.DataItem, "Value") %>' />
</ItemTemplate>
</asp:Repeater>
</div>

Code-Behind облачного элемента управления состоит из двух частей.

К первой относятся свойство Items и метод ProcessItems. Они предназначены для инициализации облачного элемента. Для каждой метки, которая будет оборажатся задано текстовое описание и размер шрифта, который будет использоваться для данной метки.

public
Dictionary<string, int> Items
{ set { ProcessItems(value); } }

private void ProcessItems(Dictionary<string, int> list)
{
cloudCtrl.DataSource = list.ToDictionary(i => i.Key, i => new
FontUnit(i.Value));
cloudCtrl.DataBind();
}

Стоит отметить что при инициализации в элемент управления передается информация об размере шрифта. На первый взгляд может показаться что было бы неплохо, что бы вместо размера шрифта сюда передавалось число определяющее, сколько раз данная метка использовалась, а уже сам контрол на основании этой информации мог бы сам определять размер шрифта. Но в таком случае было бы невозможно адаптировать элемент к различным условиям. Например, если ожидается что число использований меток будет небольшое – то размер шрифта должен изменяться быстрее, чем в случае когда количество использований предполагается большим. В иных случаях может понадобится изменять размер шрифта не по тому сколько раз встречается та или иная метка, а по некоторой другой информации, специфичной для конкретного случая. Передавая при инициализации размер шрифта – мы тем самым предоставляем право клиентскому коду самому решать когда какой размер шрифта меток должен быть.

Вторая часть представляет собой обработчик нажатия на метку и генерацию события TagClicked, которое будет обработано клентским кодом.

public class StringEventArgs : EventArgs
{
 
public string Value { get; set; }
 
public StringEventArgs() { }
 
public StringEventArgs(string val)
 
{
 
Value = val;
 
}
}

public event EventHandler<StringEventArgs> TagClicked;

protected void OnClick(object sender, EventArgs args)
{
 
var btn = sender as LinkButton;
 
if(btn!=null)
 
{
  
if (TagClicked != null)
  
TagClicked(this, new StringEventArgs(btn.Text));
 
}
}


Клиентский код



При помещении нашего облака на страницу необходимо следующую разметку:

<%@Register TagPrefix="my" TagName="TagControl" Src="~/Controls/CloudControl.ascx" %>
...
<my:TagControl OnTagClicked="OnTagClicked" runat="server" id="tagCtrl"/>

С Code-Behind дело немного сложнее, но так же все очень просто.

Инициализация облачного элемента

protected void Page_Load(object sender, EventArgs e)
{
    // получаем все метки известные системе, и информацию о том сколько раз каждая метка используется
Dictionary<string, int> dic = DataProvider.GetTagsStatistic();
// инициализируем облачный контрол
tagCtrl.Items = dic.ToDictionary(pair => pair.Key, pair => GetFontSize(pair.Value));
...
}

Обратите внимание на метод, которые устанавливает размер шрифта метки исходя из информации о том сколько раз метка была использована

private int GetFontSize(int num)
{
/* 1 использование - 8px
* 2-3 - 11px
* 4-7 - 14px
* 8-15 - 17px
* 16-31 - 20px
* и так далее
*/

int pow = 0;
while(num > 1)
{
 
pow++;
 
num = num >> 1;
}
return 8 + pow*3;
}

Обработка нажатия на метку.

protected void OnTagClicked(object sender, StringEventArgs e)
{
 
string label = e.Value;
 
// в зависимости от выбранной метки осуществляем фильтрацию наших данных
...
}

Вот пожалуй и все, облачный элемент управления готов к использованию. Понятное дело, что это только базовая функциональность. Продвигаясь дальше – можно разрисовывать метки в различные цвета в зависимости от релевантности окружения, можно добавить кнопку Reset по нажатию на которую будет осуществляться сброс текущей фильтрации, что бы пользователь смог отобразить все элементы.

Так или иначе мне кажется что использование меток в таком ключе, намного более выгодное и гибкое решение чем использование иерархий и древовидных контролов. Да и выглядит все это более дружелюбно и интуитивно для конечного пользователя.