Tag Archives: Sitecore

Sitecore + Xamarin Workbooks = Interactive Sitecore learning

Xamarin Workbooks is the perfect tool/media for interactive learning. Xamarin Workbooks provides you a blend of documentation and code that is perfect for experimentation, learning, and creating guides and teaching aids. Mainly the idea was to use it to run “Xamarin code” but it also works great for all kinds of .Net libraries, like Scott Hanselmann describes in his post, Xamarin .NET Workbooks – Interactive Computing is a stellar learning tool

The page at https://developer.xamarin.com/workbooks/ is FILLED with amazing example workbooks and lessons, and it’s growing. It has section not only on C# but Android, Games, Graphics as a concept, iOS, WPF, and so much more.

And as Scott says, we can do all kind of workbooks and it’s perfect for learning – interactive learning. So lets do some Sitecoring in the Xamarin Workbooks. 😄

First we need to install and setup Xamarin Workbooks, it’s very easy. Go to Installation and Requirements and follow instructions. (It works on Mac and Windows)

Cool, lets fire it up and select “Console”.

Now you can start playing around 🙂 In order to execute the code, hit the small play button below the “code window”.

You can also reference external dll’s, locally or by Nuget package. To do it locally:
referenceDll

By Nuget package, search for packages by browsing to File > Add Package. Adding a package will automatically bring in #r statements referencing package assemblies, allowing you to use them right away.

referenceNuget

This is very cool, I hope that the Sitecore people will take a look at the Xamarin Workbooks and add it to the Sitecore documentation. I mean interactive documentation – How cool is that 🙂

I’ve created a Sitecore.Workbook at the GitHub – Sitecore-Workbooks. Feel free to download and test it out.
Sitecore.workbook

The cool thing it’s all in markdown, here is the content from the Sitecore.Workbook file 😉

Welcome to Sitecore Workbooks

Workbooks are live documents that mix text, code and results in the same document.

Look at following code,  if you notice there is a “play icon” at the bottom after each “code window”. Hit play and the code will execute 🙂

string test = "<h1>Let's do some Xamarin Workbooks</h1>";

Now we will add the AsHtml()

test.AsHtml();

Ok let’s make a 64base image. First we need some base64 data.

string image = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i...";

We will put the image variable into a “html string” and render it as html.

string htmlString = $"<img src="{image}" height="100" />";
htmlString.AsHtml();

Time for some sitecoring

Next we will reference an external dll, we can do that by referencing them locally. We will need the Sitecore.Kernel.dll. In order to make it work, you will need to setup/create a folder with the sitecore dll.

#r "C:\Habitat\dlls\Sitecore.Kernel.dll"

Or we can reference nuget packages. Let’s grab the NSubstitute package.

#r "NSubstitute"

Ok, time for some Sitecore coding. How about we create an Item 🙂

using NSubstitute;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

Database db = Substitute.For();
ID itemId = ID.NewID;
Language language = Language.Current;
ItemDefinition definition = new ItemDefinition(itemId, "Some item name", ID.NewID, ID.NewID);
ItemData data = new ItemData(definition, language,Sitecore.Data.Version.First, new FieldList());
Item item = Substitute.For(itemId, data, db);

Sitecore Habitat

Next we will try some Sitecore.Habitat stuff. Lets play with Sitecore.Feature.Search, we will need a reference to the Sitecore.Feature.Search dll

#r "C:\Habitat\dlls\Sitecore.Feature.Search.dll"

Let’s try the PagedSearchResults and see/test that it works properly.

using Sitecore.Feature.Search.Models;

int page = 1;
int totalResults = 10;
int pagesToShow = 1;
int resultsOnPage =2;
PagedSearchResults searchResults = new PagedSearchResults(page, totalResults, pagesToShow, resultsOnPage);
double totalPages = Math.Ceiling(totalResults / (double)resultsOnPage);

searchResults.TotalPagesCount == totalPages;

Sitecore Mobile SDK for Xamarin

Lets start by adding the nuget package, Sitecore.MobileSDK.

#r "Sitecore.MobileSDK"
#r "Sitecore.MobileSDK.PasswordProvider.Interface"

Cool, ok guys. 🙂 Now we have the package, time to do some coding.

How about grab some code from method TestParseCorrectData in AuthenticateResponseParserTest at

To make it easier we will remove the NUnit stuff. Check out the code below, press play and see the result.

using System;
using System.Threading;
using System.Threading.Tasks;
using Sitecore.MobileSDK.API.Exceptions;
using Sitecore.MobileSDK.Authenticate;

var response = "{\"statusCode\":200,\"result\":{}}";
WebApiJsonStatusMessage message = AuthenticateResponseParser.ParseResponse(response, CancellationToken.None);
message.StatusCode.Equals(200);

Try to remove row “message.StatusCode.Equals(200);“ and hit play again

var response = "{\"statusCode\":200,\"result\":{}}";
WebApiJsonStatusMessage message = AuthenticateResponseParser.ParseResponse(response, CancellationToken.None);

That’s all for now folks 🙂

Advertisements

Client Tracker with Sitecore DMS

Marketplace

In my previous blog post, Track a visitors coordinates in the Sitecore DMS, I was asked if I could move it into a Sitecore package and release it on the Sitecore Marketplace.

I managed to transform the blog post to a Sitecore module and released it on Sitecore Marketplace – CLIENT TRACKER

What does it do?
Well the main idea was to track visitors/users location(using HTML5 Geolocation API) and store it in the Sitecore DMS but It can also be used to track events/goals.

I will continue working with the module – Soon there will be support for enrolling and moving visitors between States in the Engagement Automation Plans.

How to use it?
Install the package(It contains only files)
/TrackerHandler.ashx
/ClientEventTracker.js
/PageScriptSample.js
/bin/Sitecore.SharedSource.ClientTracker.dll

The package will put TrackerHandler.ashx, ClientEventTracker.js and PageScriptSample.js in the root. I strongly suggest that you move the files to proper folders. PageScriptSample.js is just a sample.

Add the ClientEventTracker.js to the page/s where you want to do the client tracking.

To send the client data you will use a Json dictionary, which supports following parameters:
Coordinates (Track current user/visitor location)
PageEventId (Track event/goal)
PageUrl (The url/page where the event/goal is fired)

It’s important that you name the dictionary to requestParamAndValues. Don’t forget to add the path to the handler, TrackerHandler.ashx.

var pathToHandler = "/CustomHandlers/TrackerHandler.ashx";
var requestParamAndValues = {};
requestParamAndValues["PageEventId"] = dataContainer.data("goal");
requestParamAndValues["PageUrl"] = dataContainer.data("pageurl");

var jsonObject = {};
jsonObject["requestParamAndValues"] = requestParamAndValues;

var analyticsEvent = new AnalyticsPageEvent(jsonObject, pathToHandler);
analyticsEvent.trigger();

Ok, let’s try it then. In following example we want to track if a user/visitor has closed an “alert box” and get the geolocation of the visitor.
Image1

First we create the event/goal in Sitecore.
Image2

Next we proceed to the widget containing the alert box. The data-goal attribute contains the item id for the “Closed Alert box” event and the data-pageurl holds the path to actual page(in order for the tracker to find the correct page row).

<div class="alert alert-info fade in" 
        data-pageurl="<%= HttpContext.Current.Request.Url.PathAndQuery %>"
        data-goal=" <%# Sandbox.Alert.Constants.Analytics.Goals.CloseAlertBox %> ">
        <button type="button" class="close" id="closeAlert" data-dismiss="alert">×</button>
        <asp:PlaceHolder runat="server" Visible="<%# !string.IsNullOrWhiteSpace(this.GetDataSourceOrContextItem().GetString(Sandbox.Spots.Constants.Fields.Alert.AlertHeader)) || Sitecore.Context.PageMode.IsPageEditorEditing %>">
            <h4>
                <sc:Text runat="server" DataSource="<%# this.GetDataSourceOrContextItem().ID %>" Field="<%# Sandbox.Spots.Constants.Fields.Alert.AlertHeader%>" />
            </h4>
        </asp:PlaceHolder>

        <sc:Text runat="server" DataSource="<%# this.GetDataSourceOrContextItem().ID %>" Field="<%# Sandbox.Spots.Constants.Fields.Alert.AlertText%>" />
    </div>

Finally we add the custom/sample javascript for the page.

var SharedSource = SharedSource || {};

jQuery(document).ready(function () {
    SharedSource.ClientTracker.DomReady();
});

SharedSource.ClientTracker = {
    DomReady: function() {

        SharedSource.ClientTracker.Init();

       jQuery("#closeAlert").click(function () {

            SharedSource.ClientTracker.TrackPageEvent();

        });


    },

    Init: function(trackUser) {

        window.pathToHandler = "/CustomHandlers/TrackerHandler.ashx";

        if (navigator)
            navigator.geolocation.getCurrentPosition(geoSuccess, geoError);

        function geoSuccess(p) {

            window.coordinates = p.coords;

            if (trackUser) {
                SharedSource.ClientTracker.TrackUserLocation(); 
            }

        }

        function geoError(error) {
            var message = "";
            switch (error.code) {
            case error.PERMISSION_DENIED:
                message = "This website does not have permission to use " + "the Geolocation API";
                break;
            case error.POSITION_UNAVAILABLE:
                message = "The current position could not be determined.";
                break;
            case error.PERMISSION_DENIED_TIMEOUT:
                message = "The current position could not be determined " + "within the specified timeout period.";
                break;
            }

            if (message == "") {
                var strErrorCode = error.code.toString();
                message = "The position could not be determined due to " + "an unknown error (Code: " + strErrorCode + ").";
            }

            Logging(message);
        };


    },
    TrackUserLocation: function() {

        if (!window.coordinates)
            return;

        var requestParamAndValues = {};
        requestParamAndValues["Coordinates"] = SharedSource.ClientTracker.StringFormat("{0},{1}", window.coordinates.latitude, window.coordinates.longitude, '1');

        var jsonObject = {};
        jsonObject["requestParamAndValues"] = requestParamAndValues;

        var analyticsEvent = new AnalyticsPageEvent(jsonObject, window.pathToHandler);
        analyticsEvent.trigger();

    },
    StringFormat: function () {
        var s = arguments[0];
        for (var i = 0; i < arguments.length - 1; i++) {
            var reg = new RegExp("\\{" + i + "\\}", "gm");
            s = s.replace(reg, arguments[i + 1]);
        }
        return s;
    },

    Logging: function (message) {
        if (typeof console == "object") {
            console.log(message);
        }
    },

    TrackPageEvent: function() {

        var dataContainer = jQuery(".alert alert-info fade in");

        var requestParamAndValues = {};
        requestParamAndValues["PageEventId"] = dataContainer.data("goal");
        requestParamAndValues["PageUrl"] = dataContainer.data("pageurl");

        if (window.coordinates)
            requestParamAndValues["Coordinates"] = SharedSource.ClientTracker.StringFormat("{0},{1}", window.coordinates.latitude, window.coordinates.longitude, '1');


        var jsonObject = {};
        jsonObject["requestParamAndValues"] = requestParamAndValues;

        var analyticsEvent = new AnalyticsPageEvent(jsonObject,  window.pathToHandler);
        analyticsEvent.trigger();
    }

};

That’s it – The event and the visitors current location will now be registered in the Sitecore DMS.

If you have questions, improvements, finding bugs – Please let me know.

That’s all for now folks 🙂

Track a visitors coordinates in the Sitecore DMS

phoneAndTabletMaxMind is great for tracking visitors – It gives you all kind of information based on the IP address.

Today when more and more users/visitors are using handheld devices with GPS for browsing the internet, I wanted to get the actual location for the visitor using HTML5 Geolocation API
Note: Since it can compromise user privacy, the position is not available unless the user approves it.

In order to get the location/coordinates, following javascript is used (window.coordinates is a global variable):

if (navigator)
    navigator.geolocation.getCurrentPosition(geoSuccess, geoError);

function geoSuccess(p) {
    window.coordinates = p.coords;
}

function geoError(data) {
    //Do some error handling
	
}

I added the geolocation to my previous post, Client side tracking with Sitecore DMS.

var Sandbox = Sandbox || {};
 
jQuery(document).ready(function () {
    Sandbox.Alert.DomReady();
});
 
Sandbox.Alert = {
    DomReady: function () {
 
		Sandbox.Alert.InitSandbox();
 
        jQuery("#closeAlert").click(function () {
 
            Sandbox.Alert.TrackCloseEvent();
 
        });
 
    },
 
	InitSandbox: function () {

        if (navigator)
            navigator.geolocation.getCurrentPosition(geoSuccess, geoError);

        function geoSuccess(p) {
            window.coordinates = p.coords;
        }

        function geoError(data) {
            //Some error handling
        }


    },
 
    TrackCloseEvent: function () {
 
        var dataContainer = jQuery(".alert alert-info fade in");
 
	    var requestParamAndValues = {};
        requestParamAndValues["PageEventId"] = dataContainer.data("goal");
        requestParamAndValues["PageUrl"] = dataContainer.data("pageurl");
 
		if (window.coordinates)
			requestParamAndValues["Coordinates"] = Sandbox.Alert.StringFormat("{0},{1}", window.coordinates.latitude, window.coordinates.longitude, '1');

 
        var jsonObject = {};
        jsonObject["requestParamAndValues"] = requestParamAndValues;
 
        var analyticsEvent = new AnalyticsPageEvent(jsonObject);
        analyticsEvent.trigger();
    },
	
	StringFormat : function () {
        var s = arguments[0];
        for (var i = 0; i < arguments.length - 1; i++) {
            var reg = new RegExp("\\{" + i + "\\}", "gm");
            s = s.replace(reg, arguments[i + 1]);
        }
        return s;
    }
};

In the aspx page, ClientEventTracker, we need to add some lines of code (TrackerService):

namespace Sandbox.Tracking.Presentation
{
    public partial class ClientEventTracker : System.Web.UI.Page
    {
        protected void Page_Init(object sender, EventArgs e)
        {
            Context.Response.ContentType = "application/javascript";
            TriggerEvent(this.Context);
        }
 
        private static void TriggerEvent(HttpContext context)
        {
 
            string jsonData = context.Request["jsonData"];
 
            InputData inputData = ConvertJsonToObjectService.Convert<InputData>(jsonData);
 
            if (!inputData.ContainsParamkey(InputDataKeys.PageUrl))
                return;
 
            if (!inputData.ContainsParamkey(InputDataKeys.PageEventId))
                return;
 
            Tracker.StartTracking();
            Tracker.CurrentPage.Cancel();
 
	        TrackerService.SetCurrentVisitorCoordinates(inputData.GetValueByKey(InputDataKeys.Coordinates));
 
            TrackerService.RegisterEventToAPage(inputData.GetValueByKey(InputDataKeys.PageEventId), inputData.GetValueByKey(InputDataKeys.PageUrl));
 
            Tracker.Submit();
        }
    }
}

TrackerService is defined here:

namespace Sandbox.Tracking.Infrastructure
{
    public class TrackerService
    {

        public static void RegisterEventToAPage(string eventId, string url)
        {

            if (string.IsNullOrWhiteSpace(eventId))
                return;

            if (string.IsNullOrWhiteSpace(url))
                return;

            if (!Tracker.IsActive)
                return;

            PageEventItem pageEvent = new PageEventItem(Sandbox.Framework.SitecoreEnhancements.SitecoreItemRepository.Get(eventId));

            VisitorDataSet.PagesRow pagesRow = Tracker.CurrentVisit.GetPages().LastOrDefault(p => p.Url.Contains(url));

            if (pagesRow == null)
                return;

            pagesRow.Register(pageEvent);

        }

        public static void SetCurrentVisitorCoordinates(string coordinates)
        {

            GeoCoordinate? geoCoordinate = GeoCoordinateRepository.Get(coordinates);

            if (!geoCoordinate.HasValue)
                return;

            if (!Tracker.IsActive)
                return;

            Tracker.CurrentVisit.Latitude = geoCoordinate.Value.Latitude;
            Tracker.CurrentVisit.Longitude = geoCoordinate.Value.Longitude;

        }

    }
}

GeoCoordinate object:

namespace Sandbox.Tracking.Model
{
    public struct GeoCoordinate
    {
        private readonly double _latitude;
        private readonly double _longitude;

        public double Latitude { get { return _latitude; } }
        public double Longitude { get { return _longitude; } }

        public GeoCoordinate(double latitude, double longitude)
        {
            _latitude = latitude;
            _longitude = longitude;
        }


        public override string ToString()
        {
            return string.Format("{0},{1}", Latitude, Longitude);
        }

        public override bool Equals(Object other)
        {
            return other is GeoCoordinate && Equals((GeoCoordinate)other);
        }

        public bool Equals(GeoCoordinate other)
        {
            return Math.Abs(Latitude - other.Latitude) < double.Epsilon && Math.Abs(Longitude - other.Longitude) < double.Epsilon;
        }

        public override int GetHashCode()
        {
            return Latitude.GetHashCode() ^ Longitude.GetHashCode();
        }

    }
}

GeoCoordinateRepository:

namespace Sandbox.Tracking.Model.Repositories
{
    public class GeoCoordinateRepository
    {

        public static GeoCoordinate? Get(string latitude, string longitude)
        {
            if (string.IsNullOrWhiteSpace(latitude) || string.IsNullOrWhiteSpace(longitude))
                return null;


            string coordinates = string.Concat(latitude, ",", longitude);
            return GeoCoordinateFactory.Create(coordinates);

        }

        public static GeoCoordinate? Get(string coordinates)
        {
            if (string.IsNullOrWhiteSpace(coordinates))
                return null;

            return GeoCoordinateFactory.Create(coordinates);

        }

        public static IEnumerable<GeoCoordinate?> Get(IEnumerable<string> manyCoordinates)
        {
            return manyCoordinates.Select(Get);

        }
    }
}

Finally the GeoCoordinateFactory:

namespace Sandbox.Tracking.Model.Factories
{
    internal class GeoCoordinateFactory
    {
        internal static GeoCoordinate? Create(string coordinates)
        {

            string[] coordinateArray = coordinates.Split(',');

            if (coordinateArray.Length == 0)
                return null;

            if (string.IsNullOrWhiteSpace(coordinateArray[0]) || string.IsNullOrWhiteSpace(coordinateArray[1]))
                return null;

            return new GeoCoordinate(double.Parse(coordinateArray[0], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture), double.Parse(coordinateArray[1], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture));


        }
    }
}

That’s all for now folks 🙂

Client side tracking with Sitecore DMS

For not so long ago I wanted to track a client event for the DMS. In this case I wanted to know if a specific alert box has been closed.
Image1
I did some googling and found Sitecore Client Event Tracker on the Github. That was exactly what I needed.

I did some minor changes to it. Instead of passing a number of parameters to the javascript method, AnalyticsPageEvent, I changed it to take one parameter – a Json object.

function AnalyticsPageEvent(jsonData) {
   
    this.jsonData = jsonData;

    this.trigger = function () {
        var queryString = '';

        if (!this.jsonData) {
            return;
        }

        queryString += '&' + 'jsonData' + '=' + JSON.stringify(this.jsonData);

       
        var url = '/components/Tracking/Presentation/ClientEventTracker.aspx' + '?ra=' + eventTracker.randomstring() + queryString;
        eventTracker.request(url);

    };
}

The Json object is a serialized class with a dictionary

namespace Sandbox.Framework.SitecoreSpecific.Model
{
    [DataContract]
    public class InputData
    {
        [DataMember(Name = "requestParamAndValues")]
        public Dictionary<string, string> RequestParamAndValues { get; set; }


        public bool ContainsParamkey(InputDataKeys inputDataKeys)
        {
            return RequestParamAndValues != null && RequestParamAndValues.ContainsKey(inputDataKeys.ToString());
        }

        public string GetValueByKey(InputDataKeys inputDataKeys)
        {

            string value;
            RequestParamAndValues.TryGetValue(inputDataKeys.ToString(), out value);

            return value;
        }

        public void SetValueByKey(InputDataKeys inputDataKeys, string value)
        {
            if (RequestParamAndValues.ContainsKey(inputDataKeys.ToString()))
                RequestParamAndValues[inputDataKeys.ToString()] = value;
            else
                RequestParamAndValues.Add(inputDataKeys.ToString(), value);
        }
    }
}

The keys for the dictionary are defined in an enum

namespace Sandbox.Framework.SitecoreSpecific
{
    public enum InputDataKeys
    {
        None = 0,
        IpAddress = 1,
        Coordinates = 2,
        Language = 3,
        Device = 4,
        Browser = 5,
        PageEventId = 6,
        PageEventName = 7,
        PageEventText = 8,
        PageEventKey = 9,
        PageEventData = 10,
        PageUrl = 11,
        EngagementPlan = 12,
        EngagementPlanState = 13
    }
}

The event/goal in Sitecore:
Image2

The widget containing the alert box. The data-goal attribute contains the item id for the “Closed Alert box” event and the data-pageurl holds the path to actual page(in order for the tracker to find the correct page row).

<div class="alert alert-info fade in" 
        data-pageurl="<%= HttpContext.Current.Request.Url.PathAndQuery %>"
        data-goal=" <%# Sandbox.Alert.Constants.Analytics.Goals.CloseAlertBox %> ">
        <button type="button" class="close" id="closeAlert" data-dismiss="alert">×</button>
        <asp:PlaceHolder runat="server" Visible="<%# !string.IsNullOrWhiteSpace(this.GetDataSourceOrContextItem().GetString(Sandbox.Spots.Constants.Fields.Alert.AlertHeader)) || Sitecore.Context.PageMode.IsPageEditorEditing %>">
            <h4>
                <sc:Text runat="server" DataSource="<%# this.GetDataSourceOrContextItem().ID %>" Field="<%# Sandbox.Spots.Constants.Fields.Alert.AlertHeader%>" />
            </h4>
        </asp:PlaceHolder>

        <sc:Text runat="server" DataSource="<%# this.GetDataSourceOrContextItem().ID %>" Field="<%# Sandbox.Spots.Constants.Fields.Alert.AlertText%>" />
    </div>

The javascript for the widget:

var Sandbox = Sandbox || {};

jQuery(document).ready(function () {
    Sandbox.Alert.DomReady();
});

Sandbox.Alert = {
    DomReady: function () {

        jQuery("#closeAlert").click(function () {

            Sandbox.Alert.TrackCloseEvent();

        });

    },

    TrackCloseEvent: function () {

        var dataContainer = jQuery(".alert alert-info fade in");

        var requestParamAndValues = {};
        requestParamAndValues["PageEventId"] = dataContainer.data("goal");
        requestParamAndValues["PageUrl"] = dataContainer.data("pageurl");

        var jsonObject = {};
        jsonObject["requestParamAndValues"] = requestParamAndValues;

        var analyticsEvent = new AnalyticsPageEvent(jsonObject);
        analyticsEvent.trigger();
    }
};

The javascript for the tracking.

var eventTracker = false;

function AnalyticsPageEvent(jsonData) {
   
    this.jsonData = jsonData;

    this.trigger = function () {
        var queryString = '';

        if (!this.jsonData) {
            return;
        }

        queryString += '&' + 'jsonData' + '=' + JSON.stringify(this.jsonData);

       
        var url = '/components/Tracking/Presentation/ClientEventTracker.aspx' + '?ra=' + eventTracker.randomstring() + queryString;
        eventTracker.request(url);

    };
}

function EventTracker() {
    this.request = function (url) {
        var script = new ClientEventScript(url, true);
        script.load();
    };

    this.randomstring = function () {
        var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        var text = "";

        for (var i = 0; i < 32; i++) {
            text += possible.charAt(Math.floor(Math.random() * possible.length));
        }

        return text;
    };
}

function ClientEventScript(src, async) {
    this.src = src;
    this.async = async;

    this.load = function () {
        var script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = this.src;
        script.async = this.async;

        var ssc = document.getElementsByTagName('script')[0];
        ssc.parentNode.insertBefore(script, ssc);
    };
}

eventTracker = new EventTracker();

The aspx page, ClientEventTracker, (which is requested from the EventTracker-method) will register the tracking

namespace Sandbox.Tracking.Presentation
{
    public partial class ClientEventTracker : System.Web.UI.Page
    {
        protected void Page_Init(object sender, EventArgs e)
        {
            Context.Response.ContentType = "application/javascript";
            TriggerEvent(this.Context);
        }

        private static void TriggerEvent(HttpContext context)
        {

            string jsonData = context.Request["jsonData"];

            InputData inputData = ConvertJsonToObjectService.Convert<InputData>(jsonData);

            if (!inputData.ContainsParamkey(InputDataKeys.PageUrl))
                return;

            if (!inputData.ContainsParamkey(InputDataKeys.PageEventId))
                return;

            Tracker.StartTracking();
            Tracker.CurrentPage.Cancel();

            VisitorDataSet.PagesRow pageRow = Tracker.CurrentVisit.GetPages().FirstOrDefault(p => p.Url.Contains(inputData.GetValueByKey(InputDataKeys.PageUrl)));

            if (pageRow == null)
                return;

            PageEventItem pageEvent = null;

            if (!string.IsNullOrWhiteSpace(inputData.GetValueByKey(InputDataKeys.PageEventId)))
                pageEvent = new PageEventItem(Sandbox.Framework.SitecoreEnhancements.SitecoreItemRepository.Get(inputData.GetValueByKey(InputDataKeys.PageEventId)));

            if (pageEvent == null) 
                return;

            pageRow.Register(pageEvent);

            Tracker.Submit();
        }
    }

That’s all for now folks 🙂

Make your own Droplink control for the Sitecore Page Editor

Like I mentioned in previous post, How to make a customized control for the SitecorePageEditor,  there are some basic controls missing… One of them is the Droplink control.

So let’s make one 🙂

I have a page with a “typical” column layout, I want my widget/spot to be flexible so it can fit/span over one or many columns. In this case it’s a registration form.

Registration form spans over 2 columns
RegistrationWidgetColumn

Registration form spans over 1 column
RegistrationWidgetColumn1

In the Page Editor the column layout for the widget/spot is controlled with a dropdown.
RegistrationWidgetColumnPageEditor

In the Content Editor the item(registration form) has a droplink containing the column values.
ColumnSelected

The droplink contains a number of “column value” items, each “column value” has one “friendly value” and one “real value”.
The “real value” is the actual column value and the “friendly value” is what presents to the editor. In this case “1 column” is spanning over 4 columns
ColumnValue

The code for rendering the column span looks like this:

<div <%# !string.IsNullOrWhiteSpace(this.GetDataSourceOrContextItem().GetString(Sandbox.Identity.Constants.Fields.FooterTextSection.ColumnSelectorNumberOfColumns)) ? string.Format(@"class='col-md-{0}'", this.GetDataSourceOrContextItem().GetString(Sandbox.Identity.Constants.Fields.FooterTextSection.ColumnSelectorNumberOfColumns)) : string.Empty %>>

If you are wondering what the GetDataSourceOrContextItem is? It’s something I used from Brian Pedersen’s blog, http://briancaos.wordpress.com/2013/09/13/avoid-the-use-of-sitecore-context-item/.
(GetDropLinkSelectedItem and GetString are also extension methods)

Now it’s time to make the Droplink control for the Page Editor. The column layout for the widget/spot is controlled with a dropdown. That’s what we are going to do. But first I’ll do a dirty version.

<div  runat="server" visible="<%# Sitecore.Context.PageMode.IsPageEditorEditing %>">
    <span style="font-size: x-small">Number of columns it will span over</span>
    <select onchange=" var itemUri = new Sitecore.ItemUri('<%# this.GetDataSourceOrContextItem().ID.ToShortID() %>',
        '<%# this.GetDataSourceOrContextItem().Language.ToString() %>',
        '<%# this.GetDataSourceOrContextItem().Version.ToString() %>',
        '<%# new ID(this.GetDataSourceOrContextItem()[FieldIDs.Revision]).ToShortID() %>');
        Sitecore.WebEdit.setFieldValue(itemUri,
        '<%# Sandbox.Identity.Constants.Fields.FooterTextSection.ColumnSelectorNumberOfColumns.ToShortID() %>',
        this.options[this.selectedIndex].value);">
        <option value="" <%# string.IsNullOrWhiteSpace(this.GetDataSourceOrContextItem().GetString(Sandbox.Identity.Constants.Fields.FooterTextSection.ColumnSelectorNumberOfColumns)) ? "selected" : string.Empty   %>>No column</option>
        <option value="4" <%# this.GetDataSourceOrContextItem().GetString(Sandbox.Identity.Constants.Fields.FooterTextSection.ColumnSelectorNumberOfColumns).Equals("4") ? "selected" : string.Empty   %>>1 column</option>
        <option value="8" <%# this.GetDataSourceOrContextItem().GetString(Sandbox.Identity.Constants.Fields.FooterTextSection.ColumnSelectorNumberOfColumns).Equals("8") ? "selected" : string.Empty   %>>2 columns</option>
        <option value="12" <%# this.GetDataSourceOrContextItem().GetString(Sandbox.Identity.Constants.Fields.FooterTextSection.ColumnSelectorNumberOfColumns).Equals("12") ? "selected" : string.Empty   %>>3 columns</option>
    </select>
</div>

Previous post, How to make a customized control for the Sitecore Page Editor, explains what Sitecore.WebEdit.setFieldValue does.

Let’s redo the dirty version to a nice control. It will look like this when it’s finished.

<%@ Register TagPrefix="mycontrol" Namespace="Sandbox.Framework.Controls.PageEditor" Assembly="Sandbox.Framework.Controls" %>

<div runat="server" visible="<%# Sitecore.Context.PageMode.IsPageEditorEditing %>">
    <span style="font-size: x-small">Number of columns it will span over</span>
    <mycontrol:PageEditorDropLinkControl runat="server"
            DataSource="<%# this.GetDataSourceOrContextItem().ID %>"
            Field="<%# Sandbox.Newsletter.Constants.Fields.RegistrationForm.ColumnSelectorDropdown %>"
            EmptyText="No column"
            DropLinkItemTextField="<%# Sandbox.Design.Constants.Fields.DesignColumn.ColumnValueFriendly %>"
            DropLinkItemTemplate="<%# Sandbox.Design.Constants.Templates.DesignColumn %>"
            DropLinkItems="<%# Sandbox.Design.Model.Repositories.PageColumnRepository.PageColumnsSettingsItem().GetChildren()  %>" />

</div>

Finally! Here is the code for the “PageEditorDropLinkControl”

public class PageEditorDropLinkControl : FieldControl
{
    private readonly SafeDictionary<string> _parameters = new SafeDictionary<string>();


    public string EmptyText { get; set; }

    public ID DropLinkItemTextField { get; set; }

    public IEnumerable<Item> DropLinkItems { get; set; }

    public ID DropLinkItemTemplate { get; set; }

    protected override void DoRender(HtmlTextWriter output)
    {

        if (string.IsNullOrWhiteSpace(Field))
            throw new InvalidOperationException("Field property is required. All field web controls require the field name to be set.");

          
        FieldRenderer fieldRenderer = new FieldRenderer
        {
            Parameters = GetParameters(),
            DisableWebEditing = DisableWebEditing
        };


        StringBuilder stringBuilderOnChange = new StringBuilder();

        StringBuilder stringBuilderSelect = new StringBuilder();

        stringBuilderOnChange.AppendFormat(@"var itemUri = new Sitecore.ItemUri('{0}','{1}','{2}','{3}');", GetItem().ID.ToShortID(),
            GetItem().Language, GetItem().Version, new ID(GetItem()[FieldIDs.Revision]).ToShortID());

        stringBuilderOnChange.AppendFormat(@"Sitecore.WebEdit.setFieldValue(itemUri,'{0}',this.options[this.selectedIndex].value);", Field);

        stringBuilderSelect.AppendFormat(@"<select onchange=""{0}"" >", stringBuilderOnChange.ToString());
        stringBuilderSelect.AppendLine();
        stringBuilderSelect.AppendFormat(@"<option value='' {0} >{1}</option>", GetItem().GetDropLinkSelectedItem(new ID(Field)) == null ? "selected" : string.Empty, EmptyText);
        stringBuilderSelect.AppendLine();
            
        foreach (Item dropLinkItem in DropLinkItems.Where(dropLinkItem => dropLinkItem.IsDerived(DropLinkItemTemplate)))
        {
            stringBuilderSelect.AppendFormat(@"<option value='{0}' {1} >{2}</option>", dropLinkItem.ID, GetItem().GetDropLinkSelectedItem(new ID(Field)).ID.Equals(dropLinkItem.ID) ? "selected" : string.Empty, dropLinkItem.GetString(DropLinkItemTextField));
            stringBuilderSelect.AppendLine();
        }           

            
           
        stringBuilderSelect.AppendLine();
        stringBuilderSelect.Append("</select>");

        output.Write(stringBuilderSelect.ToString());


    }

    protected override Item GetItem()
    {
        if (this.Item != null)
            return this.Item;

        return !string.IsNullOrWhiteSpace(this.DataSource) ? Sitecore.Context.Item.Database.GetItem(this.DataSource) : base.GetItem();
    }

    private string GetParameters()
    {
        PopulateParameters(_parameters);
        return WebUtil.BuildQueryString(_parameters, false);
    }


}

That’s all for now folks 🙂

How to make a customized control for the Sitecore Page Editor

For not so long ago I saw the Content Editor as the only true tool that the editors should use for administering websites.

But then I had a revelation – the DMS – and the Page Editor showed me the way.

The Page Editor feels really nice and it’s so easy to work with. It has some nice controls like the sc:text, sc:Image and sc:link. But where are the others? I mean the sitecore dropdown control or something simple as the checkbox control…

First I thought it must be easy to implement. I could always use the sc:EditFrame but then I realized it’s just another way of using the Content Editor, it’s not good enough. If you are using the Page Editor then you should go all the way.

So I started to analyze how Sitecore did in the Page Editor and I found the magic ingredient, the javascript method: Sitecore.WebEdit.setFieldValue

This little baby creates a hidden field which the Page Editor reads when a “Save” or “Save/Close” button is clicked.

The parameters for the method are:

  • Sitecore.ItemUri
  • FieldId
  • The actual value

The Sitecore.ItemUri needs following parameters:

  • Item Id
  • LanguageVersion
  • Revision.

So now I could finally start doing some cool stuff.

In this case I have a page that presents a simple registration form. I wanted to give the editors the choice to decide if the text fields needs to be validated.

RegistrationWidget

The item itself contains some labels and checkboxes (for the validation).

RegistrationWidgetContentEditor

I was thinking of presenting the validation choices as checkboxes in the Page Editor , but unfortunately the Page Editor seems to interfere with checkbox control (html). I couldn’t get it to work so instead I used a select box containing “true/1” and “false/0” values.

RegistrationWidgetPageEditor

The code for the validation “checkboxes” in the Page Editor:

<span runat="server" visible="<%# Sitecore.Context.PageMode.IsPageEditorEditing %>">
    <span style="font-size: x-small">Should field be validated?</span>
    <select onchange="var itemUri = new Sitecore.ItemUri( '<%# this.GetDataSourceOrContextItem().ID.ToShortID() %>', 
        '<%# this.GetDataSourceOrContextItem().Language.ToString() %>', 
        '<%# this.GetDataSourceOrContextItem().Version.ToString() %>', 
        '<%# new ID(this.GetDataSourceOrContextItem()[FieldIDs.Revision]).ToShortID() %>'); 
        Sitecore.WebEdit.setFieldValue( itemUri, 
        '<%# Sandbox.Newsletter.Constants.FieldIDs.RegistrationForm.RegistrationFormFirstNameShouldBeValidated.ToShortID() %>', 
        this.options[this.selectedIndex].value); ">
        <option value="1" <%# this.GetDataSourceOrContextItem().GetCheckBoxValue(Sandbox.Newsletter.Constants.FieldIDs.RegistrationForm.RegistrationFormFirstNameShouldBeValidated) ? "selected" : string.Empty %>>Yes</option>
        <option value="0" <%# !this.GetDataSourceOrContextItem().GetCheckBoxValue(Sandbox.Newsletter.Constants.FieldIDs.RegistrationForm.RegistrationFormFirstNameShouldBeValidated) ? "selected" : string.Empty %>>No</option>
    </select>
</span>

If you are wondering what the GetDataSourceOrContextItem is? It’s something I used from Brian Pedersen’s blog, http://briancaos.wordpress.com/2013/09/13/avoid-the-use-of-sitecore-context-item/.

(Method: GetCheckboxValue is also an extension method)

Everything worked and I was a happy man, but then I realized that I have to repeat this for every input field in the registration form. Not pretty at all…

So I looked how Sitecore did with their “sc:controls” and finally I came up with a control which will look like this when used in the Page Editor:

<%@ Register TagPrefix="mycontrol" Namespace="Sandbox.Framework.Controls.PageEditor" Assembly="Sandbox.Framework.Controls" %>

<span runat="server" visible="<%# Sitecore.Context.PageMode.IsPageEditorEditing %>">
  <span style="font-size: x-small">Should field be validated?</span>
  <mycontrol:PageEditorCheckboxControl TrueText="Yes" FalseText="No" DataSource="<%# this.GetDataSourceOrContextItem().ID %>" Field="<%# Sandbox.Newsletter.Constants.FieldIDs.RegistrationForm.RegistrationFormFirstNameShouldBeValidated %>" runat="server" />
</span>

And here is the complete code for the customized control:

public class PageEditorCheckboxControl : FieldControl
{
    private readonly SafeDictionary<string> _parameters = new SafeDictionary<string>();

    public string TrueText { get; set; }

    public string FalseText { get; set; }

    protected override void DoRender(HtmlTextWriter output)
    {

        if (string.IsNullOrEmpty(Field))
            throw new InvalidOperationException("Field property is required. All field web controls require the field name to be set.");

        FieldRenderer fieldRenderer = new FieldRenderer
        {
            Parameters = GetParameters(),
            DisableWebEditing = DisableWebEditing
        };


        StringBuilder stringBuilderOnChange = new StringBuilder();

        StringBuilder stringBuilderSelect = new StringBuilder();

        stringBuilderOnChange.AppendFormat(@"var itemUri = new Sitecore.ItemUri('{0}','{1}','{2}','{3}');", GetItem().ID.ToShortID(),
            GetItem().Language, GetItem().Version, new ID(GetItem()[FieldIDs.Revision]).ToShortID());

        stringBuilderOnChange.AppendFormat(@"Sitecore.WebEdit.setFieldValue(itemUri,'{0}',this.options[this.selectedIndex].value);", Field);

        stringBuilderSelect.AppendFormat(@"<select onchange=""{0}"" >", stringBuilderOnChange.ToString());
        stringBuilderSelect.AppendLine();
        stringBuilderSelect.AppendFormat(@"<option value='0' {0} >{1}</option>", GetItem()[Field] == "0" ? "selected" : string.Empty, FalseText);
        stringBuilderSelect.AppendFormat(@"<option value='1' {0} >{1}</option>", GetItem()[Field] == "1" ? "selected" : string.Empty, TrueText);
        stringBuilderSelect.AppendLine();
        stringBuilderSelect.Append("</select>");

        output.Write(stringBuilderSelect.ToString());


    }

    protected override Item GetItem()
    {
        if (this.Item != null)
            return this.Item;

        return !string.IsNullOrWhiteSpace(this.DataSource) ? Sitecore.Context.Item.Database.GetItem(this.DataSource) : base.GetItem();
    }

    private string GetParameters()
    {
        PopulateParameters(_parameters);
        return WebUtil.BuildQueryString(_parameters, false);
    }

}

That’s all for now folks 🙂