Experience Sitecore ! | All posts tagged 'CD'

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

Editing content on a CD server. Part 1. MVC ajax request to controller

Imagine the situation, when you need to have a page with an updatable text, for instance:

This div becomes editable as you click it

Now the next logical step would be to fire on blur client event (it happens when out focus out of div, ending the editing mode) and send changed content somewhere to the back end. Something simple like jQuery snippet below can handle that:


$('#editField').blur(function () {
        
    $.ajax({
        url: 'some/backend/url/to/post',
        data: { name: value },
        type: 'post',
        success: function () {
            // handle success 
        },
        error: function () {
            // handle error
        }
    });
});

So far, so good. The very next question would be - how do I create an endpoint in Sitecore to support that ajax post request and how do I pass the data and handle positive and negative outcomes? I assume, the back end should have some MVC controller action, that does some back end job of storing my data and returning JSON object back to client script.

So in order to make this work we register MVC routes, this is referenced from Application_Start event handler and is usually implemented in App_Start folder.
    public class Application : Sitecore.Web.Application
    {
        protected void Application_Start()
        {
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(
                 name: "ajax",
                 url: "api/Ajax/{action}/{id}",
                 defaults: new { controller = "Ajax", 
                                 action = "DefaultActionMethodName", id = UrlParameter.Optional }
               );
        }
    }

This route binds all the /api/Ajax requests to be served by AjaxController class. But there is one more setting you require to do in order for your request to go the right direction - in config file set up a custom handler that will intercept that types of requests:

  
    ...
    
    ...

Controller action method being called is specified by caller, and will call DefaultActionMethodName as a default fallback if missing, passing id is optional. Here is the controller:

    public class AjaxController : Controller
    {
        [HttpPost]
        public ActionResult PostComment(string id, PostCommentViewModel model)
        {
            // implement backend logic here

            return Json();
        }
    }

 Controller accepts id as a parameter automatically resolved from URL, as specified in route we configured earlier. It also accepts and binds JSON object that we send as data into PostCommentViewModel object that automatically comes into controller as second parameter. Here is its implementation:

    public class PostCommentViewModel
    {
        [AllowHtml]
        public string Comment { get; set; }
    }
So, we can now finalize jQuery snippet that handles blur effect and sends data to controller. I have intentionally simplified it using external JavaScript objects, for clarity of understanding, you would normally avoid using global JavaScript variables in production code. These objects are used to keep state between ajax calls and to call server only when content is modified indeed. 
If there was an error on server, script retains previous value. If request worked out successfully with a status code 200 (OK) the we store updated value into <div> tag.  
var contents = $('#editField').html();
var id = '@Html.Sitecore().CurrentItem.ID';

var data = {};

$('#editField').blur(function () {
    if (contents != $(this).html()) {

        $.ajax({
            url: '/api/Ajax/PostComment/' + id,
            data: { Comment: $('#editField').html().trim() },
            type: 'post',
            success: function () {
                contents = $('#editField').html();
                var k = 0;
            },
            error: function () {
                $(this).html(contents);
            }
        });

        contents = $(this).html();
    }
});

On the server side there is not much to do with it - just save to database and return the result. Here is the final code of PostComment action of AjaxController:

 public class AjaxController : BaseController
    {
        [HttpPost]
        public ActionResult PostComment(string id, PostCommentViewModel model) // change to HtmlString
        {
            //Response.StatusCode = 500; 

            Database database = Sitecore.Context.Database;
            var item = database.GetItem(id);

            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                item.Editing.BeginEdit();
                try
                {
                    item.Fields["Comment"].Value = model.Comment;
                }
                finally
                {
                    item.Editing.EndEdit();
                }
            }

            item = database.GetItem(id);
            var val = item.Fields["Comment"].Value;

            return Json(id + " |" + model.Comment);
        }
    }

Editing content on a CD server. Part 2. Event Queue to sync data with master

... this blog is a next step after Editing content on a CD server. Part 1. MVC ajax request to controller.

Now we got a question on how to implement server logic to store the value. It may seem pretty straightforward for a moment - get item by its ID, update the field and return result. But in fact it's not, due to Sitecore's architectural principles.


Our page is running on a CD (content delivery server) and the item we're trying to update comes from CD database (traditionally called "web"), so if we edit and update item on "web" database - we'll get into situation when "web" contains updated version, while "master" doesn't. Publishing is one-way process of copying items from CM to CD databases (or simply from "master" to "web") so with next publishing we may overwrite updated item in web with outdated previous version from master. Writing directly to CM database is a violation of architectural principles, while it is technically possible on your developer "default" Sitecore installation, in real world Sitecore CD servers do not keep "master" connection string, making this process impossible. So, what should we do in that case?


Luckily, there are several ways of solving this scenario, each has its own pros and cons, so it is worth of thinking well ahead which (and if) is applicable to your solution.

Solution 1: Allocate a separate database in parallel with web, to store all user editable content. Normal items and non-user content will remain in web database, as normally. Here is the great article describing that approach:

Solution 2: Employ Sitecore Event Queue no notify CM about CD changes, so that as soon CM receives update event, it updates itself with the latest change coming from CD and then re-publishes the change across the rest of CD databases in order to keep them in sync. We describe this approach below.


Sitecore has a mechanism that is called remote events and allows communication between instances. This is implemented via "core" database that has EventQueue table that is monitored by a minor periods of time (like 2 seconds). We always have connection string reference to core database on our CD, at least for authorization / security purpose but also to support Event Queue that servers transport for publishing operations.

So, the process of saving comment on back end now looks fairly complicated - as post request comes - we still save the changes into CD ("web") database, on CM we also add an additional handler to item:saved event that executes custom code with event handler, that updates the same item on master database. And finally, you may programmatically re-publish updated item to other CD instances (if many), that do not have an updated version yet.

Now let's look at the code that implements all described below. AjaxController will include additional code right before returning JSON back to browser:

UpdateCommentEvent evt = new UpdateCommentEvent();
evt.Id = item.ID.ToString();
evt.FieldName = fieldName;
evt.Value = database.GetItem(id).Fields[fieldName].Value;
                 
Sitecore.Eventing.EventManager.QueueEvent<UpdateCommentEvent>(evt); 

What we're doing here is just queueing an event. UpdateCommentEvent is a custom event written by us, it is normal C# class, but please pay attention to DataContract and DataMember attributes. This is required for serialization purposes. Here is how UpdateCommentEvent is defined within the code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;

namespace Website.Code.Events
{
    [DataContract]
    public class UpdateCommentEvent
    {
        [DataMember]
        public string Value { get; set; }

        [DataMember]
        public string Id { get; set; }

        [DataMember]
        public string FieldName { get; set; }

        public UpdateCommentEvent(string id, string fieldName, string value)
        {
            Value = value;
            Id = Id;
            FieldName = fieldName;
        }

        public UpdateCommentEvent()
        {
        }
    }
}

And, of course, we define UpdateCommentEventArgs that comes along with our new event:

namespace Website.Code.Events
{
    public class UpdateCommentEventArgs : EventArgs, IPassNativeEventArgs
    {
        private UpdateCommentEvent _evt;

        public UpdateCommentEventArgs(UpdateCommentEvent evt)
        {
            _evt = evt;
        }

        public string Id
        {
            get { return _evt.Id; }
        }

        public string FieldName
        {
            get { return _evt.FieldName; }
        }

        public string Value
        {
            get { return _evt.Value; }
        }
    }
}
Implementation on CM side: first of all we need need to specify a new hook.

    
 ...   

Here's the code referenced by hook specified above. We subscribe to our event and once it arrives - we call Run method that arranges local (for CM environment) event:

using System;
using Sitecore.Events.Hooks;
using Sitecore.Eventing;

namespace Website.Code.Events
{
    public class UpdateCommentHook : IHook
    {
        public void Initialize()
        {
            // and now raise event locally
            EventManager.Subscribe<UpdateCommentEvent>(new Action<UpdateCommentEvent>(UpdateCommentEventHandler.Run));
        }
    }
}

We need to specify new local event called updatecomment:remote in the configuration as associate it with a handler method:

    
    
    

And here is OnUpdateCommentRemote event handler that fires on CM. It gets event arguments, casts them to UpdateCommentEventArgs and extracts data out of arguments. In order to update an item on CM we require 3 parameters: Item ID, field name to be updated and the value to be updated with. All three are stored in event arguments so now we are able to update item on master database.

namespace Website.Code.Events
{
    public class UpdateCommentEventHandler
    {
        /// 
        /// The method is the method that you need to implement as you do normally
        /// 
        public virtual void OnUpdateCommentRemote(object sender, EventArgs e)
        {
            if (e is UpdateCommentEventArgs)
            {
                var args = e as UpdateCommentEventArgs;
                var master = Sitecore.Configuration.Factory.GetDatabase("master");

                using (new SecurityDisabler())
                {
                    var itemOnMaster = master.GetItem(args.Id);

                    using (new EditContext(itemOnMaster))
                    {
                        itemOnMaster[args.FieldName] = args.Value;
                    }
                }

// also do publishing to other CD here, if required
} } // This methos is used to raise the local event public static void Run(UpdateCommentEvent evt) { UpdateCommentEventArgs args = new UpdateCommentEventArgs(evt); Event.RaiseEvent("updatecomment:remote", new object[] { args }); } } }
If there are multiple CD environments (at least one apart from the one where we edit the comment) you will also need to re-publish from CM to those CDs in order to keep them all in sync.

So, in these two blog posts we described how to make an ajax MVC call to controller on back-end server and update user editable content on CD environment keeping it in-sync with other environments. Hope this post helps you to understand Sitecore architecture better.

See also (additional references):

Sitecore.Support.RemoteEventLogging
Tweak to log all item:saved:remote events of EventQueue that were just processed.
https://bitbucket.org/sitecoresupport/sitecore.support.remoteeventlogging/wiki/Home