Sunday, August 2, 2015

Mastering Sitecore MVC Forms : CRUD that works on both Management and Delivery servers


The Use Case

This is a very common scenario; you need to build a form that has the ability to create or update items in Sitecore after a user has input data and clicked the submit button.

This sounds pretty straightforward right? Well, it gets a bit tricky when you don't have direct access to the master database. For example, if you are trying to perform CRUD operations on a security hardened content delivery server, you don't have access to the master database, and this will fail.

Most folks will create a new web service on their management server that they could consume to handle this. A good option, but something extra to build and maintain.

In this post, I am going to show you how to make your form logic "smart" enough to be able to create or update items even when they are posting from content delivery servers.

The Form

Let's start out by looking at a simple form where we ask a user to input some basic information so that they can receive a reminder about an event. We intend to use this data to create an item in the master database so that a scheduled task running in the management instance will determine when to email them to remind them about the event.

The Model

The model is pretty simple. As you can tell, the class' properties almost match the form's input.

1:  public class ReminderViewModel : ModelBase<Item>  
2:  {  
3:      public ReminderStrings FormStringsAndLinks { get; set; }  
4:      public Event RemindEvent { get; set; }  
5:      public string EventId { get; set; }  
6:      public string FirstName { get; set; }  
7:      public string LastName { get; set; }  
8:      public string Email { get; set; }  
9:      public bool Consent { get; set; }  
10: }  

FormsStringsAndLinks and ReminderEvent are complex properties used to display some form input strings, links and the event information as shown above.

The Client Script

We are using AJAX to post our form. The following JavaScript snippet is an AngularJS / jQuery mix where there is a validation check on the form after the user clicks the schedule reminder button. If it's valid, we build a view model object, and post it to the SubmitReminder action in my Forms controller.

1:  if (isValid) {  
2:        var reminderViewModel = {};  
3:        reminderViewModel.firstname = $scope.txtreminderfirstname;  
4:        reminderViewModel.lastname = $scope.txtreminderlastname;  
5: = $scope.txtreminderemail;  
6:        reminderViewModel.eventId = jQuery("#hdnEventId").val();//Angular doesn't like hidden fields with guids  
7:        reminderViewModel.consent = $scope.chkreminderconsent ? $scope.chkreminderconsent : false;  
8:        $"/api/mysite/forms/SubmitReminder", reminderViewModel )  
9:          .success(function () {  
10:            jQuery("#reminder-app").slideUp("slow");  
11:            jQuery("#reminder-set").removeAttr("style");  
12:          })  
13:          .error(function () {  
14:            alert("An error occurred while adding you to our reminder list.");  
15:          });  
16:      }  

The Submit Reminder Action

The action method takes the model object, and then passes it to a business layer method in line 5. If it is successful, it will use some key values to identify the contact in xDB. The xDB piece is beyond the scope of this post.

1:  [HttpPost]  
2:      public bool SubmitReminder(ReminderViewModel reminderViewModel)  
3:      {  
4:        var reminderManager = new ReminderManager();  
5:        var success = reminderManager.CreateEventReminder(reminderViewModel);  
6:        if (success)  
7:        {  
8:          var identifiedContact = new IdentifyContact  
9:          {  
10:            FirstName = reminderViewModel.FirstName,  
11:            LastName = reminderViewModel.LastName,  
12:            Email = reminderViewModel.Email  
13:          };  
14:          ExperienceProfile.ContactIdentifier = reminderViewModel.Email;  
15:          ExperienceProfile.UpdateProfile(identifiedContact);  
16:        }  
17:        return success;  
18:      }  

Nothing special happening in the business layer method. It is strictly a wrapper around the repository in this case.

1:  public bool CreateEventReminder(ReminderViewModel reminderViewModel)  
2:      {  
3:        return _eventRepository.CreateEventReminder(reminderViewModel);  
4:      }  

Handling the CRUD

Config Settings

In our set up, we have an element where we define the application instance type, default database and the url for the management instance.

We also have other config nodes that help to dynamically switch the Solr or Lucene indexes that we are querying against based on the environment we have set. We won't get too much into this, but it's basically a static configuration class that we access within our query methods.

1:  <  
2:     applicationInstance="management"  
3:     defaultDomain=""  
4:     defaultDatabase="master"  
5:     siteName=""  
6:     managementInstanceUrl="">  
7:     <indexes>  
8:      <index name="buckets" management="sitecore_master_index" delivery="sitecore_web_index" />  
9:      <index name="products" management="commerce_products_master_index" delivery="commerce_products_web_index" />  
10:     <index name="media" management="scene7_media_master_index" delivery="scene7_media_web_index" />  
11:    </indexes>  
12: </>  

On a delivery server, the applicationInstance would be set to "delivery" and the defaultDatabase to "web".

Create Event Reminder

The first method that gets accessed in our repository is CreateEventReminder, as shown above in the quick snippet from the business code.

1:  public bool CreateEventReminder(ReminderViewModel reminderViewModel)  
2:      {    
3:        return Settings.ApplicationInstance == InstanceType.Management ? CreateEventReminderMaster(reminderViewModel) : PostEventToManagement(reminderViewModel);  
4:      }  

Line 3 is a ternary that determines if we are on the delivery or management instance, and either passes the model object to the CreateEventReminderMaster method if we are on the management server, or to the PostEventToManagement method if we are on the delivery server.

Creating the Item in the Master Database

The CreateEventReminderMaster method below, as it's name indicates, creates items in the master database. There is a check to determine if the item already exists in the database, before creating it. Nothing really fancy here.

1:  public bool CreateEventReminderMaster(ReminderViewModel reminderViewModel)  
2:      {  
3:        //We are on the management server  
4:        var masterService = new SitecoreService("master");  
5:        var destinationFolder = masterService.Database.GetItem(new ID(Sitecore.Configuration.Settings.GetSetting("EventReminderFolderID")));  
6:        if (destinationFolder == null)  
7:        {  
8:          return false;  
9:        }  
10:        var itemName = ItemUtil.ProposeValidItemName(reminderViewModel.Email + reminderViewModel.EventId);  
11:        if (!EventReminderExists((itemName)))  
12:        {  
13:          if (!EventReminderExistsFallback((itemName)))  
14:          {  
15:            var newUser = new Event_Reminder  
16:            {  
17:              Name = itemName,  
18:              First_Name = reminderViewModel.FirstName,  
19:              Last_Name = reminderViewModel.LastName,  
20:              Email = reminderViewModel.Email,  
21:              Communications = reminderViewModel.Consent,  
22:              Event = new Guid(reminderViewModel.EventId)  
23:            };  
24:            using (new SecurityDisabler())  
25:            {  
26:              masterService.Create(destinationFolder, newUser);  
27:            }  
28:          }  
29:        }  
30:        return true;  
31:      }  

This is an example of an item that has been created:

Posting Data From the Delivery to the Management Server

This is the magic method.

As it turns out, it's quite simple. All we are doing here is posting the model object back over to the exact same controller and action on the management server. You can see on line 4 where I am building the url to the controller's action.

On line 6, I created a clean DTO so that I didn't run into any issues when serializing my object to JSON.

Finally, I used the WebClient class' UploadData method to post my model back over to the management server as shown on line 14.

1:  public bool PostEventToManagement(ReminderViewModel reminderViewModel)  
2:      {  
3:        //This will get executed if you are on a delivery (CD) server  
4:        var controllerUrl = $"{Settings.ManagementInstanceUrl}/api/mysite/forms/SubmitReminder";  
5:        //Create a clean DTO Object to send over  
6:        var reminderDTO = new {reminderViewModel.FirstName, reminderViewModel.LastName, reminderViewModel.Email, reminderViewModel.EventId, reminderViewModel.Consent};  
7:        using (var client = new WebClient())  
8:        {  
9:          client.Headers["Content-type"] = "application/json";  
10:          // serializing the reminderViewModel object in JSON notation  
11:          var jss = new JavaScriptSerializer();  
12:          var json = jss.Serialize(reminderDTO);  
13:          //Post to controller on cm  
14:          client.UploadData(controllerUrl, "POST", Encoding.UTF8.GetBytes(json));  
15:          return true;  
16:        }  
17:      }  


With these pieces in place, we can summarize the events that follow after a user has completed their input and clicks the submit button:

Management Server Instance

If the setting indicates that we are on the management server, the object is passed to the CreateEventReminderMaster method where it creates the item in the master database.

Delivery Server Instance

If the setting indicates that we are on the delivery server, the object is passed to the PostEventToManagement method where is posts the model over to the management server where the process starts again.

Once on the management server, the object ends up in the CreateEventReminderMaster method where the item is created in the master database.

Final Thoughts

I hope that you find this concept useful as you start working on your own custom MVC forms that require you to make changes to the master database from all instances.

Please share your thoughts and comments.

Happy Sitecoring!


Post a Comment