Showing posts with label Content. Show all posts
Showing posts with label Content. Show all posts

9 December 2014

How to access profiles, profile keys through code

Last month I went to the Sitecore DMS fundamentals training as I wanted to have a taste of what and how marketing team will use all the tools available. Some really interesting materials and tools that we are coding for but unfortunately don't really use too often...

During the training session, we had a chat about Profile card, Profile keys and how we are assigning those to a content item. The example given there was for a Cycling Shop. We could create some profile keys for "Beginner", "Amateur" and "Professional". Obviously, you will have different product that will be for Professional: Helmets, Bikes and even GoPro (why not...). Well that gave me an idea for this blog posts and the following on:
  1. How to access those keys and Profile card when we access the item.
  2. How to retrieve Related items based on profile keys - we will continue with that on a later post...
Before starting, I just wanted to point you to a few blogs and posts which helped me a lot:


So first let see how the data are stored in the sitecore item:

When you assign the Profile card and keys in Sitecore you will  click on the "Edit Profile Card associated with the item":

  

Next you will certainly edit the profile you want to assign to the content item and edit the values


Once you have done that, you will notice that those data are stored on the Content Item. The field you are looking for is (enable the standard fields): Tracking ( - or __tracking)

 If you view it with the raw values, you will see that all the data are stored in XML format:

I have pasted the formated XML here for readability:


 < profile id="{1E05D43D-BF07-403D-893D-552FCFBF2BD0}" name="MyTest" presets="general|100||">
  < key name="Digital" value="2" />
  < key name="Traditional" value="2" />
 < /profile>

Since those data are in a field, that means we can access them throuh code... Here are a few methods to show you how you can retrieve the ContentProfiles or just the ContentProfiles name or if you prefer to get the profile Keys... The important thing here is to get the Field "__tracking" and cast it to a Sitecore.Analytics.Data.TrackingField. This field will give you all the required information you need for Campaigns, events, and Profiles... From this field, you should now be able to extract the different profiles associated with the item and then from each profile you can access every keys...

        /// 
        /// Return the list of all profiles for the content Item
        /// 
        /// 
        /// 
        public static IEnumerable GetProfiles(Item item)
        {
            if (!AnalyticsSettings.Enabled)
            {
                return new List();
            }

            Field field = item.Fields["__tracking"];
            if (field == null)
                return new List();

            ContentProfile[] profiles = (new TrackingField(field)).Profiles;
            if (profiles == null)
                return new List();

            return profiles.ToList();
        }

        /// 
        /// Return the list of all profile names associated with the content Item
        /// Where Keys are defined. This is to make sure we are not returning the profiles that are not assigned
        /// 
        /// 
        /// 
        public static IEnumerable GetProfileNames(Item item)
        {
            if (!AnalyticsSettings.Enabled)
            {
                return new List();
            }

            Field field = item.Fields["__tracking"];
            if (field == null)
                return new List();

            ContentProfile[] profiles = (new TrackingField(field)).Profiles;
            if (profiles == null)
                return new List();

            return profiles.Where(p => GetProfileKeys(p,1).Count()>0).Select(i => i.Name).ToList();
        }


        /// 
        /// Get the keys contains in the Content Profile with a minimum score
        /// 
        /// 
        /// 
        /// 
        public static IEnumerable GetProfileKeys( ContentProfile profile, int minimumScore)
        {
            var matchedKeys = new List();

            foreach (ContentProfileKeyData profileKey in profile.Keys)
            {
                if (profileKey.Value >= minimumScore)
                {
                    matchedKeys.Add(profileKey);
                }
            }
            return matchedKeys;
        }

        /// 
        /// Get the key names contains in the Content Profile with a minimum score
        /// 
        /// 
        /// 
        /// 
        public static IEnumerable GetProfileKeyNames(ContentProfile profile, int minimumScore)
        {
            var matchedKeys = new List();

            foreach (ContentProfileKeyData profileKey in profile.Keys)
            {
                if (profileKey.Value >= minimumScore)
                {
                    matchedKeys.Add(profileKey.Name);
                }
            }
            return matchedKeys;
        }

        /// 
        /// Get the Key Names that are define in the Item profiles with a minimum score
        /// 
        /// 
        /// 
        /// 
        public static IEnumerable GetProfileKeyNames(Item item, int minimumScore)
        {
            List keyNames = new List();

            IEnumerable profiles = GetProfiles(item);
            foreach (var profile in profiles)
            {
                keyNames.AddRange(GetProfileKeyNames(profile, minimumScore));
            }
            return keyNames.Distinct();
 
        }

Now on the next post, we will see how we could use that to get the related content items based on Profile Keys...

3 December 2014

Sitecore 7.x MVC and placeholder key with Capital Letter

Another post today about Sitecore MVC and Placeholder Key. We have this project using Sitecore MVC where the placeholders are defined with capital letters, Eventhough the Page Editor recommendation guide (page 14 to 15) does not say that using Capital Letters will create issues in MVC.... well.... it does. So even if not said directly on the guide: use lower case for your placeholder name... But just in case it is too late and you need to page editor, you can read the following...

In MVC, adding a placeholder in the renderings with capital letters will not create any problem when binding any rendering to this placeholder. However, Sitecore will not resolve your Placeholder settings correctly and you will not see the "Add Here" buttons neither the "allowed controls"

To summarise, if you do something like:

        @Html.Sitecore().Placeholder("SideColumn")

You will end up withthe SideColumn placeholder not even displayed, even if you add capital letters in the Placeholder Settings:



That would not really bother me as I would need to rename all placeholder into lower case and voila!!! But I wanted to find out if there would be an easier way and something that would allow me to have capital letter in the Name... When trying to see where this is falling, you have to check how the Chrome Data are rendered. If you look at the pipeline "GetChromeData", you can see that there is a processor for Placeholder:

      < getChromeData>
        < processor type="Sitecore.Pipelines.GetChromeData.Setup, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetFieldChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetWordFieldChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetRenderingChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetEditFrameChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel" />
      < /getChromeData>


When following the trail from the GetPlaceholderChromeData you will find that the code is calling another pipeline "GetPlaceHolderRenderings"
                        GetPlaceholderRenderingsArgs getPlaceholderRenderingsArgs = new GetPlaceholderRenderingsArgs(text, layout, args.Item.Database)
                        {
                            OmitNonEditableRenderings = true
                        };
                        CorePipeline.Run("getPlaceholderRenderings", getPlaceholderRenderingsArgs);

with the pipeline looking like:
      < getPlaceholderRenderings>
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.GetPredefinedRenderings, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.RemoveNonEditableRenderings, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.GetPlaceholderRenderingsDialogUrl, Sitecore.Kernel" />
      < /getPlaceholderRenderings>



Continuing the trail... go to the first processor: GetAllowedRenderings. In there you will find out how the PlaceholderItem is getting retrieved
    using (new DeviceSwitcher(args.DeviceId, args.ContentDatabase))
    {
     item = Client.Page.GetPlaceholderItem(args.PlaceholderKey, args.ContentDatabase, args.LayoutDefinition);
    }


If you look inside this method you will see that those placeholder Item are retrieved from the cache:
 PlaceholderCache placeholderCache = PlaceholderCacheManager.GetPlaceholderCache(database.Name);
 Item item = placeholderCache[placeholderKey];
 if (item != null)
 {
  return item;
 }
 int num = placeholderKey.LastIndexOf('/');
 if (num >= 0)
 {
  string key = StringUtil.Mid(placeholderKey, num + 1);
  item = placeholderCache[key];
 }
 return item;

You will note that the placeholderCache.IsKeyCaseSensitive is false, and this return null if you are passing the key as capital letters... So if we look at how the cache is build:
public virtual void Reload()
{
 lock (FieldRelatedItemCache.Lock)
 {
  this.itemIDs = new SafeDictionary();
  string query = string.Format("{0}//*[@@templateid = '{1}']", this.ItemRoot.Paths.FullPath, this.ItemTemplate);
  Item[] array = this.Database.SelectItems(query);
  if (array != null)
  {
   Item[] array2 = array;
   for (int i = 0; i < array2.Length; i++)
   {
    Item item = array2[i];
    string text = item[this.FieldKey];
    if (!string.IsNullOrEmpty(text))
    {
     string cacheKey = this.GetCacheKey(text);
     if (!this.itemIDs.ContainsKey(cacheKey))
     {
      this.Add(text, item);
     }
    }
   }
  }
 }
}

with the method GetCacheKey where you can see that if the IsKeyCaseSensitive is false the cache key will be lower case...
protected virtual string GetCacheKey(string key)
{
 if (this.IsKeyCaseSensitive || string.IsNullOrEmpty(key))
 {
  return key;
 }
 return key.ToLowerInvariant();
}


Also inside the Add method there is also a call to the GetCacheKey... So
 string cacheKey = this.GetCacheKey(key);
 lock (FieldRelatedItemCache.Lock)
 {
  this.itemIDs[cacheKey] = item.ID;
 }

That means the key is stored as capital Letter if you have define your field in the placeholder settings as capital letter. So what happens where we try to retrieve the item from the cache is that we are passing the Key as Capital letter but it is stored as lower case...
public virtual Item this[string key]
{
 get
 {
  Item result;
  lock (FieldRelatedItemCache.Lock)
  {
   ID itemId;
   if (!this.itemIDs.TryGetValue(key, out itemId))
   {
    result = null;
   }
   else
   {
    Item item = this.Database.GetItem(itemId, Context.Language, Version.Latest);
    if (item == null)
    {
     this.itemIDs.Remove(key);
    }
    result = item;
   }
  }
  return result;
 }
}


So I went around this issue by adding a custom pipeline action on the GetChromeData pipeline to convert the Placeholder Key to Lower Invariant if the Key for the cache manager is not sensitive...
namespace MyProject.Business.Sitecore.Pipelines
{
    public class GetPlaceholderKeyForChromeData : GetPlaceholderChromeData
    {
        public override void Process(GetChromeDataArgs args)
        {
            if ("placeholder".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
            {
                PlaceholderCache placeholderCache = PlaceholderCacheManager.GetPlaceholderCache(args.Item.Database.Name);
                if (!placeholderCache.IsKeyCaseSensitive)
                {
                    string keyString = args.CustomData["placeHolderKey"] as string;
                    args.CustomData["placeHolderKey"] = keyString.ToLowerInvariant();
                }
            }
        }
    }
}

With the following config patch
      
< getchromedata>
        < processor patch:before="processor[@type='Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel']" type="MyProject.Business.Sitecore.Pipelines.GetPlaceholderKeyForChromeData, MyProject.Business">
      < /processor>
< /getchromedata>


Overwritting Placeholder settings on specific templates

On a previous post, I was talking about how to create a placeholder which will allow certain controls only. For that we had a 2 Columns Renderings (Main Column and Side Column) which had the following Markups and placeholders



    < div class="row">  
        < h1 class="col-sm-12">@Html.Sitecore().Placeholder("imagebanner")< /h1>  
    < /div>  
      
    < div class="row">  
         < h1 class="col-sm-12">@Html.Sitecore().Placeholder("contenttitle")< /h1>  
    < /div>  
    < div class="row">  
        < div class="col-sm-9 main-column">  
            < article class="col-sm-12">  
                @Html.Sitecore().Placeholder("contentcolumn")  
            < /article>  
        < /div>  
        < div class="col-sm-3 sidebar">  
            @Html.Sitecore().Placeholder("sidecolumn")  
        < /div>  
    < /div>  


As we saw, it was quite easy to define our Sitecore placeholder settings and allow a few controls:



Well this was really nice and handy if your templates using the 2 Columns Rendering should allow all 4 renderings: Rich Text, Image, Navigation, Related News. BUT... What if some of your templates should only allow 1 rendering? In the news content page for instance, you want to allow only the related news panel. Creating a new rendering with a different placeholder key??? no way.... Well, good thing sitecore thought about this. You can overwritte the placeholder through the presentation:

1- Define a new placeholder in Sitecore. This new placeholder will be exactly the same as per the first one Except:
  • Having a different Key: the key must be unique, so you have to use another key
  • Instead of allowing the 4 renderings, we will only allow 1: Related News Panel


2- Edit the template - or item you want to have different settings in the placeholder:

In this example we will edit a specific item but you can do it at the template level:
  • Select your item.
  • Go to the presentation
  • Select the placholder settings 
  • Click edit
   
  •  When editing, you can select "Select Existing Settings" on the Left hand side and then Select our new placeholder settings. Make sure the Placholder Key correspond to the key you were using previously


You can now save everything and go to the page editor. You will notice that on the placeholders you will not see "SideColumn" but "NewsItemSidePanel". Then if you add a new panel, you will notice only the related news panel is available:



3 May 2014

Backup using serialization before deploying packages

Well in previous post, we saw that we could generate the serialization prior installing any packages. Well the underlying goal was to actually backup the content... So let's extend a bit and see if we can do a bit more. What would be nice would be to delete the entire folder prior serialization and zipping the serialization folder once it is completed...

1- Get the serialization folder:
This is quite simple and you should be able to do it with the following code:
            string SerializationRootFolder = FileUtil.MapPath(SC.Configuration.Settings.SerializationFolder);


2- Clear the folder
So let's delete all files and subfolders... Obviously you will only be able to do that if you have the required file permission setup... Check your app pool if using network services... But for this exercies, let's say you have setup the permissions so you are allowed to delete files and folder...
        /// 
        /// Clear Serialisation folder
        /// 
        /// 
        private void ClearSerializationFolder(string SerializationRootFolder)
        {
            System.IO.DirectoryInfo downloadedMessageInfo = new DirectoryInfo(SerializationRootFolder);

            foreach (FileInfo file in downloadedMessageInfo.GetFiles())
            {
                file.Delete();
            }
            foreach (DirectoryInfo dir in downloadedMessageInfo.GetDirectories())
            {
                dir.Delete(true);
            }
        }

3- Start serializing the entire content tree

Well guess what, we do have this code from previous post
        private void BackupItemTree(string id)
        {
            Database db = SC.Configuration.Factory.GetDatabase("master");
            Item itm = db.GetItem(id);

            SC.Data.Serialization.Manager.DumpTree(itm);
        }

4- Zipping it
To zip the files, you can use different libraries available for .NET, but since Sitecore has a Zip library, why not using it...
        /// 
        /// Create the zip file of serialisation
        /// 
        /// 
        /// 
        private string CreateZipFile(string SerializationRootFolder)
        {
            string backupName = string.Format("backup_serialization_{0}.zip", DateTime.Now.ToString("yyyyMMddhhmmss"));

            var zipFile = Path.Combine(SerializationRootFolder, backupName);

            using (var fileWriter = new SC.Zip.ZipWriter(zipFile))
            {
                var files = GetAllFiles(SerializationRootFolder, SearchOption.AllDirectories);

                SC.Diagnostics.Log.Info(string.Format("Adding files ({0})", files.Count()), this);

                var length = SerializationRootFolder.Length;
                if (!SerializationRootFolder.EndsWith("\\"))
                {
                    length += 1;
                }

                foreach (var file in files)
                {
                    fileWriter.AddEntry(file.Remove(0, length), file);
                }
            }

            return zipFile;
        }

        /// 
        /// Get all files - you can include all subdirectories through search options
        /// 
        /// 
        /// 
        /// 
        protected string[] GetAllFiles(string path, SearchOption searchOption)
        {
            List files = new List();

            string searchPattern = "*.item";
            files.AddRange(Directory.GetFiles(path, searchPattern, searchOption));
            
            return files.ToArray();
        } 


OK, time to test that... SO let's install a package and watch the serialization folder:



19 February 2014

Using TDS


I wanted to do a quick post about TDS here as I am asked quite often: what is the TDS project in your solution? What is TDS? What can you do with it...
  
In most agencies you will work on projects where the team is not working in a common location. As most of you know TDS is a fantastic tools to be able to sync your databases with other team members. If like us you are working with an offshore model then TDS is awesome: you can keep track on template changes and have all of it through Visual Studio and in Version control. As expected quite a lot of Sitecore people knows about the tool and are not surprised. However, the surprise came when I am adding that I am using TDS to create my .update packages (files and Sitecore Items) for deployment into Internal QA then UAT then Prod. The package install could be automated or manual process, but that is off topic for this post. So it is only then that I realize that some of us did not use this feature from TDS - and maybe I should talk about how to use TDS for Synching content but also generate the Update Packages...

1- Synchronise content:

That is quite simple to do. Once you have created you project in Visual studio, you just need to configure the "Build" Tab to make sure TDS will point at your website. Please refer to the TDS documentation on how to create the project in Visual Studio...


Once this is setup then you will be able to use the different option from the content tree to:
  • Get Item from Sitecore
  • Synchronsie with Sitecore


When Synchronising with Sitecore you will have the following screen where you will be able to either update your sitecore instance or update the Solution file.


That will allow you to work on your task then update the solution then checkin + get the latest work from someone else and update your sitecore instance. That is awesome: all templates, layouts... are now in TDS

2- Update Packages

Now the fun starts:
You have been working on your task, as well as other developers on different area of your application. Everything looks good, everyone has been using TDS correctly and checking in their code and Sitecore items... Well it is time to deploy to TEST. I am skipping the debate of automated or manual process for later. So for the sake of this post Let say I will deploy manually. So I am pretty sure as most of us did it in the past: open the desktop > create package > select what we need... Well if you were working with TDS you could configure the TEST "configuration" so instead of connecting to your test site, yu want to create the update package:

  • Right click on your TDS project and select properties
  • Then switch the Configuration to TEST as per the below:

  • Note that the Build Tab is left empty.
  • Go to the Update Package tab, and start filling in the information for your package:

  • Once setup, if you switch the configuration profile in Visual Studio and Right click on any Sitecore Item in TDS, you will note that none of the "Get Sitecore Item" "Sync with Sitecore" are available. This is because you do not want to connect to a sitecore instance, you just want to generate a package


  •  Now, I can hear you say: Hold on, what will be included in my package? Well the answer is quite simple: what you want to be included. For that, you will need to right click on your TDS project and select "Deployment Property Manager".

This will open up the pane as per the following screen, where you can decide what to include. As a preference, I usually include all my templates, all my layout and renderings but None of the Content Item to make sure those does not get overwritten... Then again, from time to time, I do include some items with the "deploy" property "Once" instead of always. This is to make sure that this item will be created if it does not exist but nothing will happen if it does...


Well the last step is simple: build your TDS project using the TEST profile and ... voila:
You have your update packages on the TDS project (file system...):
  
Once those packages are generated, you can install them manually through the Sitecore update installer: http://mysite/sitecore/admin/updateinstallationwizard.aspx

Or you can look for automate the deployment...

NOTE: There is one thing I did not talked abut on this post is the Config replacement, which I am planning on talking in a later post