Lumber room
29 May 2009

Раскраска Calendar List

Введение


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



Реализация


Первая мысль была — попробовать раскрасить из обьектной модели.
но… как выяснилось, красится умеют только элементы отображаемые на месячном календаре 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.


К нему был создан класс для обеспечения выбора списка для раскраски,

image

 [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 файл, который открывал окно

image

Содержимое окна — файл 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=&#60;%# DataBinder.Eval(Container,"TabIndex")%&#62;
     >
       <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 сайта


Что получилось в итоге:

image

Что дает такой подход?



На кодеплексе есть проект, где подобный календарь реализован через подключение javascript на страницу.

Минусы:

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


Что дает такой подход:

  • Делаю версию, в которой в определенные ячейки вставляются Silverlight-компоненты.
  • Субьективно, увеличилась скорость рендеринга календаря (сейчас весь биндинг идет из кода)
  • Точно так же можно теперь контролл заменить шаблонным контроллом.


Данная работа была проведена совместно с товарищем, который к сожалению не имеет аккаунта на Хабре, но очень желает на него попасть, буду признателен за предоставленный ему инвайт :)

Спасибо за внимание!

0
303 0
Leave a comment