Введение
Недавно встала задача сделать раскрашиваемый по значению поля в списке календарь.
При этом задача немного осложнялась тем, что было необходимо не только раскрашивать лист, но и применять различные стили к блокам на календаре.
Реализация
Первая мысль была — попробовать раскрасить из обьектной модели.
но… как выяснилось, красится умеют только элементы отображаемые на месячном календаре 0_0
Кстати, эти элементы имеют еще одну особенность, подробнее будет рассказано ниже.
Для начала, был создан WebPart для отображения легенды.
public class StyledCalendar : System.Web.UI.WebControls.WebParts.WebPart
{
#region [ Properties ]
[Personalizable, Browsable(false)]
public string FieldID
{
get;
set;
}
[Personalizable, Browsable(false)]
public string ListID
{
get;
set;
}
[Personalizable, Browsable(false)]
public List<ItemData> Colored
{
get;
set;http://habrahabr.ru/edit/topic/60813/#
}
#endregion
// .............
}
* This source code was highlighted with Source Code Highlighter.
К нему был создан класс для обеспечения выбора списка для раскраски,

[Serializable]
public class ItemData
{
public string CSS { get; set;}
public string JavaScript { get; set; }
public string ItemGUID { get; set; }
public string ItemText { get; set; }
}
public class StyledCalendarEditorPart : EditorPart, IPostBackEventHandler
{
private DropDownList _ddlFields;
private DropDownList _ddlValues;
private DropDownList _ddlCalendars;
// Пропущено
protected override void CreateChildControls()
{
Panel groupPanel = new Panel();
_ddlFields = new DropDownList();
_ddlValues = new DropDownList();
_ddlCalendars = new DropDownList();
_txtStyleName = new TextBox();
_txtStyleName.ID = "txtStyleName";
_btnChange = new Button();
_btnChange.Text = "Change Style";
_btnChange.OnClientClick = "openStyleWindow()";
_ddlValues.Attributes.Add("onchange", "onFieldValueChange(this, '" + _txtStyleName.ClientID + "')");
_ddlCalendars.SelectedIndexChanged += new EventHandler(_ddlCalendars_SelectedIndexChanged);
_ddlCalendars.AutoPostBack = true;
if(_list != String.Empty )
_ddlCalendars.Text = _list;
_ddlFields.SelectedIndexChanged += new EventHandler(_ddlFields_SelectedIndexChanged);
_ddlFields.AutoPostBack = true;
if(_field!= String.Empty)
_ddlFields.Text = _field;
groupPanel.Controls.Add(new LiteralControl("<br>"));
groupPanel.Controls.Add(new LiteralControl("Calendars on page:<br>"));
groupPanel.Controls.Add(_ddlCalendars);
groupPanel.Controls.Add(new LiteralControl("<br>"));
groupPanel.Controls.Add(new LiteralControl("Choice Fields:<br>"));
groupPanel.Controls.Add(_ddlFields);
groupPanel.Controls.Add(new LiteralControl("<br>"));
groupPanel.Controls.Add(new LiteralControl("Field Value:<br>"));
groupPanel.Controls.Add(_ddlValues);
groupPanel.Controls.Add(new LiteralControl("<br>"));
groupPanel.Controls.Add(new LiteralControl("<br>"));
groupPanel.Controls.Add(_btnChange);
groupPanel.Controls.Add(new LiteralControl("<br>"));
groupPanel.Controls.Add(new LiteralControl("Template Preview:<br>"));
_lbPreview = new Label();
_lbPreview.Text = MakePreview();
groupPanel.Controls.Add(_lbPreview);
groupPanel.Controls.Add(new LiteralControl("<br>"));
groupPanel.Controls.Add(new LiteralControl("Style Name:<br>"));
groupPanel.Controls.Add(_txtStyleName);
this.Controls.Add(groupPanel);
}
// Пропущено
}
* This source code was highlighted with Source Code Highlighter.
В проект был добавлен js файл, который открывал окно

Содержимое окна — файл SetStyle.aspx положили в LAYOUTS
Собственно можно переходить к организации раскраски.
Опытным путем (рефлектор) было установлено откуда берутся шаблоны для списка
TEMPLATES\CONTROLTEMPLATES\DefaultTemplates.aspx
В нем можно найти что-то вроде:
<SharePoint:RenderingTemplate ID="CalendarViewMonthItemMultiDayTemplate" runat="server">
<Template>
<div class="<%# DataBinder.Eval(Container,"DivClass","")%>" dir="<%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.Direction",""))%>" >
<table border="0" width="100%" cellspacing=0 cellpadding=0 dir="<%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.Direction",""))%>" >
<tr>
<td class="<%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.BackgroundColorClassName",""))%>"
onmouseover="this.className='<%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.BackgroundColorClassName",""))%>sel';"
onmouseout="this.className='<%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.BackgroundColorClassName",""))%>';"
href="<%# SPHttpUtility.HtmlUrlAttributeEncode(DataBinder.Eval(Container,"DataItem.DisplayFormUrl",""))%>?ID=<%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.ItemID",""))%>"
ONCLICK="GoToLink(this);return false;" target="_self"
>
<a onfocus="OnLink(this)"
href="<%# SPHttpUtility.HtmlUrlAttributeEncode(DataBinder.Eval(Container,"DataItem.DisplayFormUrl",""))%>?ID=<%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.ItemID",""))%>"
ONCLICK="GoToLink(this);return false;" target="_self"
tabindex=<%# DataBinder.Eval(Container,"TabIndex")%>
>
<b><%# SPHttpUtility.HtmlEncode(DataBinder.Eval(Container,"DataItem.Title","{0:G}"))%></b>
</a>
</td>
</tr>
</table>
</div>
</Template>
</SharePoint:RenderingTemplate>
* This source code was highlighted with Source Code Highlighter.
И так для каждого типа Item'а календарика.
Менять этот файл не нужно. Можно создать в папке CONTROLTEMPLATES свой собственный файл с любым названием. Sharepoint ищет шаблон по ID, но проверяет тип контролла-шаблона, поэтому отнаследоваться от самого шаблона нельзя — он sealed :((
Содержимое:
<SharePoint:renderingtemplate id="CalendarViewWeekItemTemplate" runat="server">
<Template>
Любое содержимое item'a
</Template>
</SharePoint:renderingtemplate>
* This source code was highlighted with Source Code Highlighter.
Первоначально, была мысль добавить Binding, но кроме зарезервированных полей достучатся ни до каких зл-тов списка было нельзя :(
Потом был написан Templated Control, внутрь которого была вставлена часть таблицы.
К сожалению, исходников этого дела не осталось так как данный механизм не работал на
элементах связаных с месячным календарем, в частности на CalendarViewMonthItemMultiDayTemplate
Судя по отладчику когда приходил биндинг, при этом почти все поля контролла были равны null.
В других контроллах все работало 0_o
Тогда концепция была изменена — был сделан новый не шаблонный контролл который жестко генерил html код в Template, при этом получая данные из списка через объектную модель Sharepoint.
В итоге код шаблона свелся к:
<SharePoint:renderingtemplate id="CalendarViewWeekItemTemplate" runat="server">
<Template>
<myCompany:MyCompanyCalendarViewDayItemCtrl ID="myCompanyCalendarViewDayItemCtrl1" runat="server">
</myCompany:MyCompanyCalendarViewDayItemCtrl>
</Template>
</SharePoint:renderingtemplate>
* This source code was highlighted with Source Code Highlighter.
Реализация класса:
public class RenderItemData
{
public string ItemName { get; set; }
public string ItemStyle { get; set; }
public string ItemDisplayFormUrl { get; set; }
public string ItemId { get; set; }
public string ItemTitle { get; set; }
public string ItemDefaultBackground { get; set; }
public string ItemDirection { get; set; }
}
[ToolboxData("<{0}:MyCompanyCalendarViewMonthItemCtrl runat=server></{0}:MyCompanyCalendarViewMonthItemCtrl>")]
public class MyCompanyCalendarView : WebControl
{
public static Dictionary<string, RenderItemData> _dict;
RenderItemData _item;
private string _dispType;
[Bindable(true)]
[Category("Appearance")]
[Localizable(true)]
protected string DisplayType
{
get
{
return _dispType;
}
set
{
_dispType = value;
}
}
public MyCompanyCalendarView()
{
if (_dict == null)
{
_dict = new Dictionary<string, RenderItemData>();
}
}
protected override void OnLoad(EventArgs e)
{
WebPartManager wp = WebPartManager.GetCurrentWebPartManager(this.Page);
StyledCalendar cal = null;
foreach (WebPart part in wp.WebParts)
{
cal = part as StyledCalendar;
if (cal != null)
{
break;
}
}
if (cal == null)
{
return;
}
SPCalendarItem calItem = (SPCalendarItem)((Microsoft.SharePoint.WebControls.SPCalendarItemContainer)(this.Parent)).DataItem;
_item = new RenderItemData();
_item.ItemTitle = calItem.Title;
_item.ItemId = calItem.ItemID;
_item.ItemDisplayFormUrl = calItem.DisplayFormUrl;
_item.ItemDefaultBackground = calItem.BackgroundColorClassName;
_item.ItemDirection = calItem.Direction;
using (SPWeb web = SPContext.Current.Web)
{
SPList list = web.Lists[new Guid(cal.ListID)];
string fieldValue = String.Empty;
string[] recId = calItem.ItemID.Split(new char[] { '.' });
foreach (SPListItem listItem in list.Items)
{
if (recId.Length > 1)
{
if (listItem.ID.ToString() == recId[0])
{
fieldValue = listItem[new Guid(cal.FieldID)].ToString();
break;
}
}
else
if (listItem.ID.ToString() == calItem.ItemID)
{
fieldValue = listItem[new Guid(cal.FieldID)].ToString();
break;
}
}
_item.ItemName = fieldValue;
foreach (ItemData data in cal.Colored)
{
if (data.ItemText == _item.ItemName)
{
_item.ItemStyle = data.CSS;
}
}
// Сделано для устранения особенности биндинга на месячный Item - пропадают все все данные полей.
if (_dict.ContainsKey(calItem.DisplayFormUrl + _item.ItemId.ToString()))
{
_dict.Remove(calItem.DisplayFormUrl + _item.ItemId.ToString());
}
_dict.Add(calItem.DisplayFormUrl + _item.ItemId.ToString(), _item);
}
base.OnLoad(e);
}
// для примера сразу вставил код для генерации одного из Item'ов
// по идее метод - абстрактный, генерация осуществляется конкретным потомком, через перегруженный метод
protected virtual string GetInnerTemplate(RenderItemData renderData, SPCalendarItem calendarItem, SPCalendarItemContainer container)
{
StringBuilder sb = new StringBuilder();
string background = calendarItem.BackgroundColorClassName;
string bgsel = calendarItem.BackgroundColorClassName + "sel";
if (renderData != null && String.IsNullOrEmpty(renderData.ItemStyle) == false)
{
background += "_m";
bgsel += "_m";
}
sb.AppendFormat("<td class='{0}'", background);
sb.AppendFormat("onmouseover=\"this.className='{0}';\"", bgsel);
sb.AppendFormat("onmouseout=\"this.className='{0}';\"", background);
sb.AppendFormat("href='{0}?ID={1}'", calendarItem.DisplayFormUrl, calendarItem.ItemID);
sb.AppendFormat("ONCLICK='GoToLink(this);return false;' target='_self'");
sb.AppendFormat(">");
sb.AppendFormat("<a onfocus='OnLink(this)'");
if (renderData != null && !String.IsNullOrEmpty(renderData.ItemStyle))
{
sb.AppendFormat(" {0} ", renderData.ItemStyle);
}
sb.AppendFormat("href='{0}?ID={1}'", calendarItem.DisplayFormUrl, calendarItem.ItemID);
sb.AppendFormat("ONCLICK='GoToLink(this);return false;' target='_self'");
sb.AppendFormat("tabindex={0}", container.TabIndex);
sb.AppendFormat(">");
sb.AppendFormat("<b>{0:G}</b>", calendarItem.Title);
sb.AppendFormat("</a>");
sb.AppendFormat("</td>");
return sb.ToString();
}
protected override void Render(HtmlTextWriter writer)
{
CreateChildControls();
SPCalendarItemContainer cont = Parent as SPCalendarItemContainer;
SPCalendarItem item = cont.DataItem as SPCalendarItem;
RenderItemData renderData = null;
_dict.TryGetValue(item.DisplayFormUrl + item.ItemID, out renderData);
string background = item.BackgroundColorClassName;
string bgsel = item.BackgroundColorClassName + "sel";
string suffix = String.Empty;
if (renderData != null && String.IsNullOrEmpty(renderData.ItemStyle) == false)
{
suffix = "_m";
}
background += suffix;
bgsel += suffix;
StringBuilder sb = new StringBuilder();
if (renderData != null && !String.IsNullOrEmpty(renderData.ItemStyle))
{
sb.AppendFormat("<div {0} dir='{1}'>", renderData.ItemStyle, item.Direction);
}
else
{
sb.AppendFormat("<div class='{0}' dir='{1}'>", cont.DivClass, item.Direction);
}
string tableStyle = "";
switch (item.CalendarType)
{
case 0:
String.Format("class='ms-cal-tdayitem{0}'", suffix);
break;
case 1: // Week Item
tableStyle = String.Format("class='ms-cal-tweekitem{0}'", suffix);
break;
case 2:
tableStyle = String.Format("class='ms-cal-tmonthitem{0}'", suffix);
break;
default:
tableStyle = string.Empty;
break;
}
sb.AppendFormat("<table border='0' width='100%' cellspacing=0 cellpadding=0 dir='{0}' {1}>", item.Direction, tableStyle);
sb.AppendFormat("<tr>");
sb.Append(GetInnerTemplate(renderData, item, cont));
sb.AppendFormat("</tr>");
sb.AppendFormat("</table>");
sb.AppendFormat("</div>");
writer.Write(sb.ToString());
base.Render(writer);
}
}
* This source code was highlighted with Source Code Highlighter.
Кроме того, отнаследовано несколько потомков для разных типов Item'ов
собственно контроллы-потомки как раз и вставляются в шаблон.
Деплой
- Копируем все странички с шаблонами в TEMPLATES\CONTROLTEMPLATES
- Копируем страничку которая отображается во всплывающем окне в TEMPLATES\LAYOUTS
- Помещаем сборку проекта в GAC
- Прописываем нашу сборку в SafeControl сайта
Что получилось в итоге:

Что дает такой подход?
На кодеплексе есть проект, где подобный календарь реализован через подключение javascript на страницу.
Минусы:
- при ошибке JS где-нибудь на странице в левом компоненте все падает, у многих пользователей по умолчанию отключен отладчик и они ничего не видят.
- ограниченные возможности кастомизации
Что дает такой подход:
- Делаю версию, в которой в определенные ячейки вставляются Silverlight-компоненты.
- Субьективно, увеличилась скорость рендеринга календаря (сейчас весь биндинг идет из кода)
- Точно так же можно теперь контролл заменить шаблонным контроллом.
Данная работа была проведена совместно с товарищем, который к сожалению не имеет аккаунта на Хабре, но очень желает на него попасть, буду признателен за предоставленный ему инвайт :)
Спасибо за внимание!