今回は Sitecore 8 がリリースされた記念に、非公式ですが実用的なテクニックについての記事を記載してみます。

サイトコアではコンテンツを構造化して管理することから、既定ではコンテンツの階層構造がURLの構造になります。ニュース記事やプレスリリース用のコンテンツを作成する場合、ニュース -> 年 -> 月 -> 日 や ニュース -> 年 -> 月 のような構造をもったアイテムの配下に該当するアイテムを配置することが多いと思われます。通常のオペレーションでは特定の日や月や年のフォルダーを作成してからニュース記事を作成する手順になりますが、ささいなステップとはいえ、自動的に年月(日)に対応するようなアイテムが作成されて、そこに記事のアイテムを配置してほしいと考えるユーザーもいると思います。また、たとえば公開日の対象月、日などを変更するとアイテム自身を移動させる必要があるため、そのオペレーションを忘れてしまうことがあるかもしれません。

そこで、今回は、ニュース記事アイテムの特定の日付フィールドの年月日情報をもとに自動的に対応する月のフォルダアイテムに記事アイテムを移動する処理を実装してみます。

検証環境は次の通りです。

  • Sitecore 7.2 (7.Xなら動作するかもしれません)
  • Sitecore Rocks 1.3

1.今回作成するプログラムの実行結果

記事が少し長くなるので、今回作成する機能を実装するとコンテンツエディターやエクスペリエンスエディター上でどのように動作するのかを先に記載します。今回はホームアイテム上で News Article データテンプレートをもとにアイテムを作成します。

そうすると、自動的に下図のように 年月フォルダーが作成されて、News Article アイテムが移動します。 News Release Date の日付を変更すると保存時に自動的に対応する 年月フォルダ にアイテムが移動します。図ではコンテンツエディターでの実行結果を表示していますが、エクスペリエンスエディターでも同じ動作をします。

2. プロジェクトの準備

Sitecore を開発するために、 Visual Studio でプロジェクトをセットアップします。Sitecore 7.X 用のプロジェクトの準備と同じですが、復習のために簡単に説明します。Sitecore Rocks をインストールしていない場合は、 拡張機能と更新プログラムからSitecore Rocks をインストールしてください。記事作成時点で最新の Sitecore Rocksのバージョンは 1.3.1 です。

プロジェクトを新規作成します。下図のように ASP.NET Webアプリケーション プロジェクトテンプレートを選択してプロジェクトを新規作成します。今回は AdvancedNews.Web という名前でプロジェクトを作成しました。

新規 ASP.NET プロジェクト ダイアログが表示されます。 テンプレートの選択で Empty を選択して OK ボタンをクリックします。

プロジェクトが自動的に作成した Web.config が SitecoreのWebサイトに配置されないように下図のようにWeb.configをソリューションエクスプローラー上で選択してプロパティ画面で ビルド アクション を なし に設定してプロジェクトを保存します。

参照設定で、 Sitecore.Kernel.dll, Sitecore.Analytics.dll への dll参照をプロジェクトに追加して下さい。

最後に、ソリューションとSitecoreのインスタンスをバインドします。ソリューションエクスプローラーで Webプロジェクト (AdvancedNews.Web) を右クリック -> Sitecore -> Project Properties をクリックします。Sitecore Explorer Connection で、関連付ける Sitecore インスタンスを選択するか New Connection で接続をセットアップしてください。

3.コンテンツの準備

プログラムを作成する前に必要なテンプレートとコンテンツの準備を行います。Sitecore Explorer を使用して下図のような News Article テンプレートを作成します。日付フィールド News Release Date の値に従って Home/News フォルダー配下に対応する 年と月のフォルダーを作成し、月フォルダー配下に News Article アイテムを自動的に配置するようにします。 テンプレートの設定が終わったら、日本語バージョンのスタンダードバリューアイテムを作成してください。

スタンダバリューアイテムの設定で下図のように各フィールドの初期値を設定します。News Release Date フィールドは $date マクロを指定して作成した日がフィールドに設定されるようにしています。

コンテンツエディターで 作業を行います(同じことをSitecore Rocks でも行えます。)。 ホームアイテムで挿入オプションの設定(設定 -> 割り当て)を行い、ホームアイテムはいかに下図のように News Article アイテムを作成できるようにしてください。また、ホームアイテム配下に News Article アイテム配置用のフォルダー型のニュースのルートフォルダーを News という名前で作成してください。News フォルダーはいかに 年、年フォルダー配下に月のフォルダーを作成してその配下に News Article アイテムが配置されるようにします。

4.プログラムの作成

Visual Studio に戻ってプログラムを作成します。 作成した Web アプリケーションプロジェクトの配下に layouts フォルダーを作成します。 layoutsフォルダーを右クリック -> 追加 -> 新しい項目 をクリックします。Sitecore -> Layouts テンプレートカテゴリから Layout テンプレートを選択し、 NewsLayout.aspx という名前でレイアウトを作成します。下記のようにコードフロント (aspx)を編集します。NewsLayoutレイアウトは単純にNews Article の内容を出力するだけのレイアウトです。

<%@ Page Language="c#" CodePage="65001" AutoEventWireup="true" Inherits="AdvancedNews.Web.layouts.NewsLayout" CodeBehind="NewsLayout.aspx.cs" %>

<%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Kernel" %>
<%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Analytics" %>
<%@ OutputCache Location="None" VaryByParam="none" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <link href="/default.css" rel="stylesheet" />
    <sc:VisitorIdentification runat="server" />
    <title>Advanced News Sample</title>
</head>
<body>
    <form method="post" runat="server" id="mainform">
        <section>
            <article>
                <h3>
                    <sc:Text runat="server" Field="News Title" />
                </h3>
                <div>
                    <sc:Date runat="server" Field="News Release Date" />
                </div>
                <div>
                    <sc:Text runat="server" Field="News Detail" />
                </div>
            </article>
        </section>
    </form>
</body>
</html>

引き続き Web アプリケーションプロジェクトに Extensionsフォルダーを作成し、 Extensions フォルダーはいかに Events というフォルダーを作成します。Eventsフォルダを右クリック -> 追加 -> 新しい項目 をクリック してください。クラスファイルを作成します。ファイル名を MoveNewsArticleSaveEventHandler.cs で作成します。MoveNewsArticleSaveEventHandler では、item:saved 用のイベントハンドラです。News Article アイテムに修正が発生した際に News Release Dateフィールドの値を参照して対象の年月フォルダ配下に 対象のNews Article アイテムを移動させています。その際年月フォルダーが存在しなければフォルダーを作成しています。

using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.Links;
using Sitecore.Pipelines.Save;
using Sitecore.Sites;
using System;
using System.Linq;

namespace AdvancedNews.Web.Extensions.Events
{
    public class MoveNewsArticleSaveEventHandler
    {
        /// <summary>ニュースを格納するルートアイテムのID(NewsフォルダアイテムのID)</summary>
        private readonly static string NewsID = "{0E5CBC6F-C170-43EA-BBEB-87E378F8CD58}";
        /// <summary>News Article テンプレート名</summary>
        private readonly static string NewsArticleTemplateName = "News Article";
        public void OnItemSaved(object sender, EventArgs args)
        {
            // イベントを発生させたアイテムを取得
            Item item = this.GetItem(args);
            // News Article 以外であれば処理しない
            if (item.TemplateName != NewsArticleTemplateName) return;
            // News Release Date フィールドに従って、アイテムの場所を移動 
            MoveItem(item);
        }
        protected Item GetItem(EventArgs args)
        {
            Item item = Event.ExtractParameter(args, 0) as Item;
            Assert.ArgumentNotNull(item, "item");
            return item;
        }
        private void MoveItem(Item target)
        {
            // News Article アイテムを格納するルートの Home/News フォルダーのアイテムを取得
            Item root = null;
            root = Sitecore.Client.ContentDatabase.GetItem(NewsID);

            
            NewsArticle item = new NewsArticle(target);            
            DateTime? releaseDate = item.ReleaseDate;
            if (releaseDate.HasValue)
            {
                int year = releaseDate.Value.Year;
                string month = string.Format("{0:00}", releaseDate.Value.Month);
                // Newsアイテムは以下にNews Release Dateの年に対応するフォルダがなければ作成
                Item yearItem = root.GetChildren().Where(x=> x.Name.Equals(year.ToString())).FirstOrDefault();
                if (yearItem == null)
                {
                    yearItem = root.Add(year.ToString(), new Sitecore.Data.TemplateID(TemplateIDs.Folder));
                }
                // 年フォルダ配下にNews Release Date の月に対応するフォルダがなければ作成
                Item monthItem = yearItem.GetChildren().Where(x => x.Name.Equals(month)).FirstOrDefault();
                if (monthItem == null)
                {
                    monthItem = yearItem.Add(month, new Sitecore.Data.TemplateID(TemplateIDs.Folder));
                }
                // News Articleアイテムの親アイテムと対象月のフォルダーが異なれば移動
                if (!item.InnerItem.ParentID.Equals(monthItem.ID))
                {
                    item.InnerItem.MoveTo(monthItem);
                    Refresh(item.InnerItem);
                }
            }
        }
        /// <summary>
        /// コンテンツエディターかエクスペリエンスエディターでアイテムを編集しているかに
        /// 応じてツリーもしくはページのリフレッシュを行う
        /// </summary>
        /// <param name="item"></param>
        private void Refresh(Item item)
        {
            // エクスペリエンスエディターを使用している場合
            if (Context.RawUrl.StartsWith("/sitecore/shell/Applications/WebEdit/WebEditRibbon.aspx"))
            {
                UrlOptions option = (UrlOptions)UrlOptions.DefaultOptions.Clone();
                option.Site = SiteContextFactory.GetSiteContext(Context.Request.QueryString["sc_pagesite"]);
                Sitecore.Web.UI.Sheer.SheerResponse.Eval(string.Format("scForm.modified = false;window.parent.location.href='{0}'; window.parent.document.body.innerHTML='<p>redirecting...</p>';", Sitecore.Links.LinkManager.GetItemUrl(item, option)));
            }
            else
            {
                string text = string.Format("item:refreshchildren(id={0})", item.ID.ToString());
                Context.ClientPage.SendMessage(this, text);
            }

        }
    }
    /// <summary>
    /// News Article アイテムのカスタムアイテム
    /// </summary>
    public class NewsArticle : Sitecore.Data.Items.CustomItem
    {
        public NewsArticle(Item innerItem) : base(innerItem) { }

        public DateTime? ReleaseDate
        {
            get
            {
                DateField field = this.InnerItem.Fields["News Release Date"];
                if (field == null || string.IsNullOrEmpty(field.Value))
                {
                    return null;
                }
                return field.DateTime;
            }
            set
            {
                DateField field = this.InnerItem.Fields["News Release Date"];
                if (field == null) return;

                if (value.HasValue)
                {
                    field.Value = Sitecore.DateUtil.ToIsoDate(value.Value);
                }
                else
                {
                    field.Value = string.Empty;
                }
            }
        }
    }
}

2014/12/26 追記
上記ソースをそのまま使用するとパブリッシュ時にエラーが発生する可能性があります。その場合は下記コードをMoveItemメソッドの先頭に追加してください。
if (Sitecore.Context.GetSiteName() == "publisher") return;
2015/01/04 追記
動作の変更があったのか、最新のインストーラー(英語版)ではエクスペリエンスエディターの判定ロジックが上記ソースコードではうまく判定できなくなっています。Sitecore 8 の場合、サイト名がshellの場合はコンテンツエディター、それ以外の場合はエクスペリエンスエディターなどで判断するようにコードを変更してください。また、Sitecore 8の場合、エクスペリエンスエディターの場合の、Refreshメソッドの処理はコメントアウトすると動く可能性があります。

 

クラス作成後、config ファイルを App_Config\Include フォルダー配下に作成してください。ファイル名は MoveNewsArticleSaveEventHandler.config という名前で config ファイルを作成しました。下図のように configファイルのプロパティ画面を表示し、"出力ディレクトリにコピー" を"新しい場合はコピーする"を選択します。

config ファイルを編集し、次のように item:saved イベントに作成した  MoveNewsArticleSaveEventHandler を登録されるようにします。

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="item:saved">
        <handler type="AdvancedNews.Web.Extensions.Events.MoveNewsArticleSaveEventHandler, AdvancedNews.Web" method="OnItemSaved"/>
      </event>
    </events>
  </sitecore>
</configuration>

ビルドを実施し、エラーが発生しないことを確認します。プロジェクトを発行して、Sitecoreにプログラムと config ファイルを配置します。dllが bin フォルダー、 configファイルが App_config/Include フォルダーに配置されていることを確認してください。

6.動作確認

準備が整ったので、動作確認を行います。エクスペリエンスエディターで ホームアイテムを表示します。リボンでコンテンツを挿入をクリックします。

コンテンツを挿入ダイアログが表示されるので News Article を選択して sampleという名前のNews Article ページを作成します。

エクスペリエンスエディターが作成したsampleコンテンツを表示します。このとき、下図のように 作成した日付($date マクロが設定されているため) に対応する 年/月フォルダ配下にコンテンツが配置されています。

エクスペリエンスエディターで下図のように News Release Date を 2015/Jan/06 に設定して保存してみます。

エクスペリエンスエディターがリフレッシュされ、 2015 年の01月用のフォルダーはいかに sample が移動していることを確認できます。

実際に Sitecore 8の新機能の一つである、ナビゲーションバー左のツリービューナビゲーション(下図参照) を展開すると sampleアイテムが News Release Dateフィールドで設定した値に対応する 年と月のフォルダー配下に格納されていることをツリービューで確認できます。

コンテンツエディターで確認したのが下図になります。

コンテンツエディターでも同じように自動的にNews Article アイテムを移動させることができます。ホームアイテムを選択し、 挿入グループから News Article をクリックします。

test という名前で コンテンツアイテムを作成します。

下図のように作成した日付($date マクロにより) に対応する年月フォルダーはいかにコンテンツアイテムが移動します。

News Release Date フィールドをたとえば下図のように 2014/Nov/11 に変更して保存してみます。

コンテンツツリーがリフレッシュされ、変更した年月フォルダーに test コンテンツが移動していることが分かります。

7.まとめ

説明は以上です。ここで紹介したコードは Sitecore 7.Xでも同じように使えるはずです。紹介した実装ではアイテムが移動して空になったフォルダーはそのままにしていますが、空になったら削除するなどの要件に合わせて適宜コードを修正して使ってください。