This Blog Has Been Moved to blog.petersondave.com
Posted: October 22, 2014 Filed under: Uncategorized Leave a commentThis blog has been moved to http://blog.petersondave.com.
My new blog is running under Jekyll hosted on Github Pages.
Check it out and let me know what you think!
Examining a Multisite Sitecore xDb Configuration
Posted: October 14, 2014 Filed under: Sitecore, Uncategorized | Tags: Sitecore, xDb Leave a commentWith the introduction of The Experience Database (xDb) in Sitecore 7.5, MongoDB hosts the primary repository of web activity across Sitecore backed websites. Web visitors, now known as contacts, are captured along with each page view (Interactions in xDb) generated in a given browsing session. Much like its predecessor, DMS, the new xDb separates web activity by site.
When building upon xDb in a multi-site implementation, being aware of how Sitecore captures and processes this information is essential for a successful multisite configuration.
A Quick Look at Sitecore 7.5 with xDb
Contact Creation
Contacts are identified just as they were in Sitecore DMS.
A cookie, SC_ANALYTICS_GLOBAL_COOKIE, is created with a Guid uniquely identifying the contact. From there, a contact record is created. This contact will be referenced for the lifetime of the cookie as interactions are recorded against the contact.
Interactions
Site activity is captured as documents within Interactions. High level data points of Interactions include:
- Site Name
- Pages viewed with URL, item Guid
- Visit page count total
- Browser type
- Screen resolution
- Geo location data
While the structure of the data differs from DMS, as we’re now storing data as documents, commonality exists between the data points captured in DMS vs the new xDb structure.
Contact Merging
The main takeaway with xDb is Sitecore’s ability to find matching contacts and merge those contacts given a predefined value uniquely identifying visitors. In previous versions, visitors identified by their Global Session cookie were maintained as unique visitor records with DMS. Upon processing of analytics data during the SessionEnd event in xDb, contacts are merged, creating a single consolidated view of the customer.
Contact merging is useful in two specific areas:
- Multiple browsing sessions for a single site – Such as sharing a shopping cart between a session on a PC and transferring that session to a mobile device. For more information on this approach, See Nick Wesselman’s series of in-depth Sitecore 7.5 posts.
- Multisite Sitecore Implementation – Two sites sharing the same membership model, both uniquely identifying contacts in the same way.
A Multisite Example
Suppose we have a multisite implementation, Launch Sitecore and Launch SkyNet (our fictitious Launch Sitecore evil twin). Both sites follow Sitecore’s best practice recommendations for configuration in IIS, while also sharing MongoDB, Collection and Reporting databases.
For the purpose of this example, while the two sites share membership, Single Sign-On is not implemented, requiring the user to identify themselves on both sites. Having such a setup will show how the xDb implementation handles contact merging and the importance of a common contact identification strategy shared across all sites.
Browsing Session #1: Launch Sitecore
Browsing the site for the first time results in the creation of a Global Analytics cookie. If you’re familiar with DMS, this works in the same way as previous versions. The cookie is what xDb will use to tie contacts together for unique browsing sessions.
While browsing Launch Sitecore, suppose we login, recognizing the current browsing session as a single customer within our membership model. At the point in which the user is identified, the contact, who previously was anonymous is now labelled using the unique identifier. In this example, we’re using the username from the extranet domain.
Notice how the previous page views (xDb interactions) are now tagged with the contact id of the logged in user. Launch Sitecore programmatically identifying the contact is below. The line of code we’re most interested in is Tracker.Current.Session.Identify(domainUser).
string name = Sitecore.Context.User.Profile.FullName;
if (name == String.Empty) name = Sitecore.Context.User.LocalName;
Tracker.Current.Contact.Tags.Add("Username", domainUser);
Tracker.Current.Contact.Tags.Add("Full name", name);Tracker.Current.Contact.Identifiers.AuthenticationLevel = AuthenticationLevel.PasswordValidated;
Tracker.Current.Session.Identify(domainUser);
Browsing Session #2: Launch SkyNet
Upon browsing Launch SkyNet, we have a completely different Global Session Guid in our cookie.
To Launch SkyNet, we’re anonymous and in no way connected to the user identified in Launch Sitecore. As soon as we login on Launch SkyNet, using the same logic to uniquely identify the contact (extranet domain username), Sitecore will flush the contact to xDb, updating the interactions with the contact id of the recognized user.
Any updates to facets, tags, counters, automation states, and contact attributes will be auto-merged within the MergeContacts pipeline processors.
Key takeaway: Contact consolidation occurs at the point in which the current tracking session is identified via Tracker.Current.Session.Identify().
Summary
Regardless of how many sites you have running through a single instance of Sitecore, xDb processing and contact merging will consolidate contacts while maintaining the page interactions of each site. It is through this process we’re able to maintain a single view of the customer and maintain the customer lifetime value as seen by the Sitecore experience database.
Risks of splitting separate Sitecore instances to separate instances of xDb processing will result in a partial view of the customer and their relative engagement value for each site instance.
Sitecore 8 at a Glance
Posted: October 1, 2014 Filed under: Uncategorized 1 CommentWith the release of the Sitecore 8 MVP Technical Preview, many of the features showcased during Sitecore Symposium 2014 were made available for review. The focus of this post is to detail high-level changes between Sitecore 7.5 and the technical preview of Sitecore 8.
Many new components were delivered in the new version of Sitecore, as well as, renaming of existing features and packaging of popular modules used in pre-Sitecore 8 versions.
Renamed Components
- Page Editor = Experience Editor
- Marketing Center = Marketing Control Panel
- Email Campaign Manager = Email Experience Manager
New Components
- Experience Analytics
- Experience Profile
- Experience Optimization
- List Manager
- Path Analyzer
- App Center
- Executive Dashboard
New Features
- Versioned Layouts
- Web API Services (SPEAK components and building applications dependent upon Sitecore data)
Existing Features Packaged with Sitecore 8
- Federated Experience Manager (available in pre Sitecore 8 versions)
- Social (previously Sitecore Social Connected)
General Look and Feel
The look and feel of the client is much improved. After the initial load, performance feels much quicker than Sitecore 7.5 and previous versions. The Launch Pad icon at the top of the page comes in very handy when wanting to switch between the new Sitecore 8 components and content editing, or experience editing; features that you’re already used to using in pre-8 installations.
Moving through the various steps of editing content, publishing, workflow, etc. match that of previous versions — just in a different layout. Everything is pretty much where you would expect it, but with a cleaner look and feel.
Obviously, with the addition of new Sitecore 8 features, you’ll rely heavily on the Launch Pad to access these components.
Sitecore Client Enhancements
Experience Editor
The experience editor is accessible from the Sitecore Experience Platform, the start menu or directly within the content editor ribbon. Just as before, the only difference being the rename of Page Editor to Experience Editor.
Experience Optimization
Testing is now exposed as Optimization in the experience editor section of the ribbon. Also accessible from the main Sitecore Experience Platform launch pad.
Content editors can establish tests, set goals and review performance reports.
Any tests created through Experience Optimization are saved under the Marketing Center Test Lab item buckets.
An example path for a home page test:
- Item: /sitecore/system/Marketing Center/Test Lab/2014/10/01/01/11/Home 20141001T011146Z
- Template: /sitecore/templates/System/Analytics/Testing/Test Definition
The Experience Optimization dashboard exposes reports enhancing new gamification concepts to the content editing and testing experience:
Workflow
When moving content changes through Workflow, new “Approve with Test” and “Approve without Test” are available, consistently reminding content editors and decision makers to consider A/B testing at a content level.
Email Experience Manager
The layout of the email campaign manager has changed, allowing for message creation and list importing available from the same menu
List Manager
The List manager allows for creation of lists from files, or manual entry from an empty list
Experience Analytics
New to Sitecore 8, the Experience Analytics feature brings together multivariate testing results, engagement scoring and overall site tracking statistics together in one location. The result is a powerful, new array of reports coupled with date range filtering and reporting facets.
Path Analyzer
One of the new features I’ve had the most fun with so far is the Path Analyzer. Clicking on the map, zooming in and out, selecting successful and least effective paths is really quite fun. The example below is leveraging Analytics data collected from a Launch Sitecore instance:
Selecting a path by clicking on the map, leading from a mail campaign to a login page yields the results below. Clicking on an element within the funnel of a selected path visit shows the exit path from that particular page:
You can also select a path from the lists in the right navigation, narrowing down on a particular path of high importance. For instance, the selected path below is one of the most efficient full paths:
Showing the funnels of the select path:
Social
Social is delivered out of the box with Sitecore 8. Previously, this required a separate installation of the Socially Connected module from SDN.
Available from the Page Editor, the “Messages” button under “Social” allowing content editors to create, edit and post a message on a target network. Take note of the new “Social” node in the content tree directly under the Sitecore root node.
Pipeline Changes
While new pipeline processors have been added to accommodate the new Sitecore 8 features, others have been moved with Content Testing and Experience Editing in mind. Below are the main areas of change with regards to pipeline processing:
- Social related pipelines come configured out of the box.
- FXM related hooks
- Everything Analytics
- Moving of existing pipelines, such as:
- SPEAK components
- Experience Editor
- Web API request handling (higher up in the httpRequestBegin pipeline)
- RenderField (immediately after httpRequestBegin)
Content Testing
Outside of Analytics, Sitecore.ContentTesting.config changes dominate the difference in Sitecore 8. Take a look at the config patch file. Content tests need to insert themselves in areas to override existing Sitecore rendering and processing handlers, such as:
- Insert Renderings
- RenderLayout
- GetChromeData
- Data Aggregation
- Database Agent (background processing to determine if a test has reached statistical relevancy)
- Content Testing provider (used by ItemManager)
The ItemManager’s default item provider is now the content testing item provider, which is essentially a wrapper of the existing Sitecore.Data.Managers.ItemProvider overriding GetItem(). It is here where other versions/variations of items used via Content Testing are obtained for rendering.
For more information regarding ItemManager and providers, check out this post on overall Sitecore data architecture.
Settings
One final interesting remark regarding configuration. You can now override the standard server time zone if needed via the ServerTimeZone setting.
In Closing…
Sitecore 8 offers a much better editing experience. It’s faster, cleaner and easier to work with. It’ll be interesting to see the various modifications to content tests and how, as developers, we can leverage this data to build modules and further enhance the client.
Verifying Sitecore Azure Deployments Against Specified Path or File Name Too Long Error
Posted: August 15, 2014 Filed under: Azure, Sitecore | Tags: Azure, Sitecore 2 CommentsWhen running a Sitecore Azure deployment, ever see this error?
“The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters”
This is not an error specific to Sitecore.
In an attempt to better understand the Sitecore Azure API and find the files/paths preventing a successful deploy, I built the Sitecore Azure Build Verifier. This tool will execute a dry-run of a Sitecore Azure deploy, alerting you of files and folders which fail validation.
Follow the instructions within the project ReadMe to install.
Configuring a Build
What I found interesting about this problem was the fact that I had installed a test version of Sitecore 7.2 with Azure 7.2 (rev 140411) under the path C:\inetpub\wwwroot_sandbox\Azure72 — not an overly verbose instance name or root path location.
For those of you new to Azure in Sitecore, specifying the target location for builds is located within an Environment item, under the field Build Folder.
As you can see, the default is C:\inetpub\wwwroot_sandbox\Azure72\Data\AzurePackages. Sitecore Azure will, also by default, append additional folders to this path for each build instance executed from within the Azure module interface resulting in a build path such as:
C:\inetpub\wwwroot_sandbox\Azure72\Data\AzurePackages\(19) DevFabric\DevFabricLCd01Role01ScBaf20140816030333.cspkg\roles\SitecoreWebRole\approot
Quickly looking at the path above, you’ll notice references to:
- Build instance
- Environment name
- Role instance and name
- DNS host name
Quick Fix
Simply change the build folder to something with a shorter path. For example, set your path to C:\AzureBuild and you’re past this issue.
Verifying The Build
If you’re interested in which files/folders are preventing a successful deploy, install the Azure Build Verifier module. After installation, select an Azure Deployment item and right-click. Select the “Verify Deployment” option:
Upon making the selection, you’ll be presented with a dialog box displaying the list of files and paths which exceed the limits previously seen in the failed deployment:
If you’re interested in the implementation, check out the GitHub repo. Dig into the artifacts processor or the main verify class leveraging the Sitecore.Azure library to resolve its sources prior to processing.
Working With The Sitecore Azure API: Settings and Content
Posted: July 29, 2014 Filed under: Azure, Sitecore | Tags: Azure, Sitecore Leave a commentDetails within this post outline how to obtain basic settings, global variables and Azure module items from Sitecore’s Azure API. Included are also some pitfalls to be aware of when working with the library.
Settings
The Settings object allows for access to the different settings defined within Sitecore.Azure.config.
<settings> | |
<setting name="EnableEventQueues"> | |
<patch:attribute name="value">true</patch:attribute> | |
</setting> | |
<setting name="Media.DisableFileMedia"> | |
<patch:attribute name="value">true</patch:attribute> | |
</setting> | |
<setting name="Azure.EnvironmentsPath" value="/App_Data/AzureEnvironments" /> | |
<setting name="Azure.HostedServicePropertiesUpdateTime" value="00:00:20" /> | |
<setting name="Azure.DefaultUpdateCacheInterval" value="00:00:20" /> | |
<setting name="Azure.VendorsBlobContainer" value="http://cloudsettings.sitecore.net/vendors" /> | |
<setting name="Azure.VendorsStorage" value="/App_Config/AzureVendors" /> | |
<setting name="Azure.Package.NoEncryptPackage" value="false" /> | |
<setting name="Azure.RoleName" value="SitecoreWebRole" /> | |
<setting name="Azure.UiRefreshInterval" value="00:00:05" /> | |
<setting name="Azure.GetEnvironmentFileInfoBlobContainer" value="http://cloudsettings.sitecore.net/{version}-{locale}/GetEnvironmentFileInfo.html" /> | |
<setting name="Azure.HttpRequestRetries" value="3" /> | |
<setting name="Azure.TranslationsPath" value="/temp/AzureTranslations" /> | |
<setting name="Azure.ManagerDisabled" value="false" /> | |
<setting name="Azure.PublishTargetsContainer" value="publishtargets" /> | |
<setting name="Azure.TrafficManager.Enable" value="true" /> | |
<!-- Setup Logging level of Log trace of Sitecore Azure App. | |
Info - Show only common messages | |
Debug - Exception will be shown too. --> | |
<setting name="Azure.LoggingSettings.LogLevel" value="Debug" /> | |
</settings> |
Obtaining EnvironmentsPath
, for example, is exposed through the following getter:
Sitecore.Azure.Configuration.Settings.EnvironmentDefinitionsPath;
Global Variables
The GlobalVariables
collection allows for retrieval of any sc.variable
defined within the site’s web.config.
<sitecore database="SqlServer"> | |
<sc.variable name="dataFolder" value="C:\inetpub\wwwroot_sandbox\Azure72\Data"/> | |
<sc.variable name="mediaFolder" value="/upload"/> | |
<sc.variable name="tempFolder" value="/temp"/> |
Obtaining a global setting:
var dataFolder = Settings.GlobalVariables["dataFolder"];
Configuration settings is not the only data exposed through the Settings class, you can also obtain:
- Environment Definitions – A collection of all environments defined under the Azure module root item.
- Environments Root – The Azure module root item.
- Vendors – Collection of all vendors under the Azure module root item.
Obtaining Azure Item Wrappers
Looking up specific Azure items using the standard Sitecore.Data.Items.Item object is fine, however, the Sitecore Azure API offers an ORM of sorts to access this information. You can instantiate these objects by passing in their related Sitecore item.
The example below shows how you can obtain an Azure deployment item and walk up the tree by accessing each item’s parent until we reach the environment item root.
var db = Database.GetDatabase("master"); | |
// Azure Deployment | |
var azureDeploymentDataItem = db.Items[deploymentId]; | |
AzureDeploymentItem = new AzureDeploymentItem(azureDeploymentDataItem); | |
// Web Role | |
var roleDataItem = AzureDeploymentItem.InnerItem.Parent; | |
WebRoleItem = new WebRoleItem(roleDataItem); | |
// Farm | |
var farmDataItem = WebRoleItem.InnerItem.Parent; | |
FarmItem = new FarmItem(farmDataItem); | |
// Location | |
var locationDataItem = FarmItem.InnerItem.Parent; | |
LocationItem = new LocationItem(locationDataItem); | |
// Environment | |
var environmentDataItem = LocationItem.InnerItem.Parent; | |
EnvironmentItem = new EnvironmentItem(environmentDataItem); |
Obtaining Specific Azure Items
To obtain Azure specific objects, follow the patterns below to access. Take note of the pitfalls section when considering this approach.
Environment
There are multiple ways to obtain an environment from the API. You can get all environment definitions:
var environments = Settings.EnvironmentDefinitions;
Explicitly target an enviornment type. Using a local emulator as an out-of-the-box example:
var environmentDefinition = Settings.EnvironmentDefinitions.GetEnvironment("Local Emulator"); | |
var environment = Sitecore.Azure.Deployments.Environments.Environment.GetEnvironment(environmentDefinition); |
Local Emulator
is defined in the Azure Environment configuration file under App_Data\AzureEnvironments\
<?xml version="1.0" encoding="utf-16"?> | |
<EnvironmentDataStorage xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |
<EMail>devfabric@sitecore.net</EMail> | |
<EnvironmentId>fe6d565e-289b-4076-a089-3588321a836c</EnvironmentId> | |
<EnvironmentType>Local Emulator</EnvironmentType> | |
... |
Location
Once an environment is obtained, getting an instance of a location is rather simple. You can Explicitly get a location:
var location = environment.GetLocation("localhost");
Iterate over a collection of locations:
var locations = environment.Locations;
Farm
Similar to the approaches above, a single farm can be obtained by deployment type:
var farm = location.GetFarm("Delivery01", DeploymentType.ContentDelivery);
as a collection:
var farms = location.Farms;
Web Role
Obtain a single instance by name:
var role = farm.GetWebRole("Role01");
or assuming Role01 exists for a common Azure configuration:
var role = farm.WebRole01;
as a collection:
var roles = farm.WebRoles;
Azure Deployment
Obtain a single instance by deployment type:
var deployment = role.GetDeployment(DeploymentSlot.Production);
as a collection:
var deployments = role.Deployments;
Deployment Settings
The deployment object exposes deployment settings from the API as well. In the example below, we leverage the FilePathFilter
object to access deployment item settings:
public string GetExcludedDirectories() | |
{ | |
var environment = Sitecore.Azure.Deployments.Environments.Environment.GetEnvironment(Settings.EnvironmentDefinitions.GetEnvironment("Local Emulator")); | |
var location = environment.GetLocation("localhost"); | |
var farm = location.GetFarm("Delivery01", DeploymentType.ContentDelivery); | |
var role = farm.WebRole01; | |
var deployment = role.GetDeployment(DeploymentSlot.Production); | |
// get values from field "Exclude Directories" | |
return deployment.FilePathFilter.ExcludeDirectories; | |
} |
Exposing these fields:
Pitfalls
Item Creation on Object Instantiation
While the API itself is easy to use, it’s essential to understand what’s happening behind the scenes. Whenever instantiating an object deriving from the abstract class AzureEntity
, if the requested item does not exist, the framework will automatically create the item for you. I’ve seen this happen specifically for location and farm items. Make sure your name matches the entry you’re looking for, otherwise, you’ll have extra items created under the Azure module branch in the content tree.
Consider a scenario of requesting a location of MyLocation. The Azure location item does not exist. I’ll run the following code:
public Location GetNonExistingLocation() | |
{ | |
var environment = Sitecore.Azure.Deployments.Environments.Environment.GetEnvironment(Settings.EnvironmentDefinitions.GetEnvironment("Local Emulator")); | |
return environment.GetLocation("MyLocation"); | |
} |
Suppose I then request the farm MyFarm which also does not exist in content:
public Farm GetNonExistingFarm() | |
{ | |
var location = GetNonExistingLocation(); | |
return location.GetFarm("MyFarm", DeploymentType.ContentDelivery); | |
} |
This results in those items being created by the API:
Hacking Sitecore Web Forms with Razor Views for Marketers and Blade
Posted: July 24, 2014 Filed under: Sitecore, Web Forms for Marketers 5 CommentsWeb Forms for Marketers provides flexibility on part of content editors to create and manipulate simple forms for collecting user data. While there’s a great deal of flexibility on the back-end, customizing the format and layout of the form can sometimes be a more difficult task.
Knowing what some of the limitations are regarding the rendered markup of Web Forms for Marketers, I wondered how difficult it would be to override the rendering of the form to allow drastic changes to the standard look-and-feel of the out-of-the-box implementation. Sure, content edits can add CSS classes to content, but I wanted to push the envelope and see how far we could go.
Disclaimer
Now, I’ll be the first to admit when you’re looking to use a feature like Web Forms for Marketers, it’s absolutely necessary to know the strengths and limitations prior to recommending a specific implementation plan. Working closely with clients and designers is critical to a successful implementation. One that can be managed well over time by content editors, but remains on the upgrade path for future updates by Sitecore.
Introducing Razor Views for Marketers
The goal of this project is simple. Override the rendering of Web Forms for Marketers forms to take full control of the rendered markup.
Purely out of research, Razor Views for Marketers was built to extend the rendering of the form. Content remains the same, as well as the structure and templates within Sitecore. Razor Views for Marketers simply replaces how forms are rendered and how validation is conducted against view models. Page editor also works.
Why Blade?
Razor Views for Marketers uses Blade to take advantage of MVC-style razor view templating, allowing us to leverage MVC editor templates and dynamic model binding. The razor views give complete control over how fields are rendered, as well as razor views for field sections and the form itself.
Example Form
As a proof of concept, I set out to replace the “Leave a Message” form. Out-of-the-box implementation renders the form as:
With a Single-Line Text field rendering as:
<div id="main_0_centercolumn_0_form_EC97FD637B414CA48CEF55F2D4EA1916_field_1C1014623802498484904C0DACF7D6FB_scope" class="scfSingleLineTextBorder fieldid.%7b1C101462-3802-4984-8490-4C0DACF7D6FB%7d name.Your+name"> | |
<label for="main_0_centercolumn_0_form_EC97FD637B414CA48CEF55F2D4EA1916_field_1C1014623802498484904C0DACF7D6FB" id="main_0_centercolumn_0_form_EC97FD637B414CA48CEF55F2D4EA1916_field_1C1014623802498484904C0DACF7D6FB_text" class="scfSingleLineTextLabel">Your name</label> | |
<div class="scfSingleLineGeneralPanel"> | |
<input name="main_0$centercolumn_0$form_EC97FD637B414CA48CEF55F2D4EA1916$field_1C1014623802498484904C0DACF7D6FB" type="text" maxlength="256" id="main_0_centercolumn_0_form_EC97FD637B414CA48CEF55F2D4EA1916_field_1C1014623802498484904C0DACF7D6FB" class="scfSingleLineTextBox"> | |
<span class="scfSingleLineTextUsefulInfo" style="display:none;"></span> | |
<span id="main_0_centercolumn_0_form_EC97FD637B414CA48CEF55F2D4EA1916_field_1C1014623802498484904C0DACF7D6FB6ADFFAE3DADB451AB530D89A2FD0307B_validator" class="scfValidator trackevent.%7bF3D7B20C-675C-4707-84CC-5E5B4481B0EE%7d fieldid.%7b1C101462-3802-4984-8490-4C0DACF7D6FB%7d inner.1" style="color:Red;display:none;">Your name must have at least 0 and no more than 256 characters.</span><span id="main_0_centercolumn_0_form_EC97FD637B414CA48CEF55F2D4EA1916_field_1C1014623802498484904C0DACF7D6FB070FCA141E9A45D78611EA650F20FE77_validator" class="scfValidator trackevent.%7b844BBD40-91F6-42CE-8823-5EA4D089ECA2%7d fieldid.%7b1C101462-3802-4984-8490-4C0DACF7D6FB%7d inner.1" style="color:Red;display:none;">The value of the Your name field is not valid.</span> | |
</div> | |
<span class="scfRequired">*</span> | |
</div> |
Replacement Form
The Razor Views for Marketers Implementation, using the following set of Razor views:
@using System.Web.Mvc | |
@using System.Web.Mvc.Html | |
@using RazorViewsForMarketers.Helpers | |
@inherits BladeRazorRendering<RazorViewsForMarketers.Models.RazorViewForMarketersFormModel> | |
<h1>@Model.Form.Title</h1> | |
@if (Model.Form.ShowIntroduction) | |
{ | |
<p>@Model.Form.Introduction</p> | |
} | |
@using (Html.BeginForm(null, null, FormMethod.Post, new { id = "razorViewForMarketers" })) | |
{ | |
@Html.DisplayTextFor(model => model.SubmitMessage) | |
<div class="form"> | |
@Html.EditorFor(model => model.Form.Sections) | |
</div> | |
<button type="submit" value="Submit" name="Command">Submit Form</button> | |
} | |
<script src="~/Scripts/jquery-1.10.2.js"></script> | |
<script src="~/Scripts/jquery.unobtrusive-ajax.js"></script> | |
<script src="~/Scripts/jquery.validate.js"></script> | |
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script> |
Sections, implemented with editor templates:
@using System.Web.Mvc.Html | |
@model RazorViewsForMarketers.Models.WffmSection | |
<div class="section"> | |
@Html.EditorFor(model => model.Fields) | |
</div> |
and fields also have editor templates. Example of a Single-Line Text:
@using System.Web.Mvc.Html | |
@using RazorViewsForMarketers.Helpers | |
@inherits BladeRazorRenderingEditorTemplate<RazorViewsForMarketers.Models.Fields.SingleLineTextField> | |
<div class="field"> | |
<!-- model binding fields --> | |
@Html.HiddenFor(model => model.Id) | |
@Html.HiddenFor(model => model.IsRequired) | |
@Html.Hidden("ModelType", Model.ModelType) | |
<!-- model binding fields --> | |
@BladeHtmlHelper.SitecoreLabel(Html, model => model) | |
@Html.TextBoxFor(model => model.Response) | |
@BladeHtmlHelper.RequiredIndicator(model => model) | |
<p>@Model.Information</p> | |
@Html.ValidationMessageFor(model => model.Response) | |
</div> |
Through helper methods, we’re able to maintain Page Editor functionality while rendering Web Forms for Marketers fields. Hidden fields are necessary for dyanmic model binding on postback for the form.
Updated rendered output of the Single-Line Text field from the razor view above:
<div class="field"> | |
<!-- model binding fields --> | |
<input id="Form_Sections_0__Fields_0__Id" name="Form.Sections[0].Fields[0].Id" type="hidden" value="1c101462-3802-4984-8490-4c0dacf7d6fb"> | |
<input id="Form_Sections_0__Fields_0__IsRequired" name="Form.Sections[0].Fields[0].IsRequired" type="hidden" value="True"> | |
<input id="Form_Sections_0__Fields_0__ModelType" name="Form.Sections[0].Fields[0].ModelType" type="hidden" value="RazorViewsForMarketers.Models.Fields.SingleLineTextField"> | |
<!-- model binding fields --> | |
<label for="Form_Sections_0__Fields_0__field_Response">Your name</label> | |
<input id="Form_Sections_0__Fields_0__Response" name="Form.Sections[0].Fields[0].Response" type="text" value=""> * | |
</div> |
Implementation Notes
The Razor Views for Marketers core logic wraps the existing SimpleForm to perform operations such as:
- Submitting the form
- Collecting WFFM save actions
- Collecting form field control results
- Expose the form’s FormItem
The wrapper is essentially an empty shell of the form, allowing us to leverage the existing WFFM framework for submitting forms through the standard Sitecore WFFM pipelines.
A field decorator encapsulates existing WFFM field control results for form processing.
Want More?
If you’re interested in this approach, check out the repo.
Recommendations on how to contribute are included. The framework supports model binding of all existing Web Forms for Marketers fields, however, views and validators have been created to accommodate only those fields within “Leave a Message” form.
How Do I Check if a Web Forms for Marketers Field is Required on Validation?
Posted: April 10, 2014 Filed under: Sitecore, Uncategorized, Web Forms for Marketers | Tags: Sitecore WFFM 2 CommentsWhen within the context of a WFFM field custom validator, determining whether or not a field is required may not be a straight-forward task. Understand that within the context of the validator, there are two ways of obtaining an instance of a form:
1. Obtain an instance of the current SimpleForm control, find your field and check if any MarkerLabels are present
2. Obtain an instance of the WFFM form itself, find your field item and check if its Required field is checked
Approach 1: Obtain an Instance of the current SimpleForm
protected override bool OnServerValidate(string value) | |
{ | |
var validatorSettings = GetValidatorSettingsItem(); | |
Sitecore.Diagnostics.Assert.IsNotNull(validatorSettings, ValidationSettingsItemMissingMessage + ValidationSettingsItem); | |
// obtain an instance of the SimpleForm | |
var form = WebUtil.GetParent<SimpleForm>(this); | |
// obtain an instance of our field | |
var countryField = (CountryDropDownList)WebUtil.FindFirstOrDefault(form, c => c is CountryDropDownList && string.Compare((c as CountryDropDownList).Result.FieldName, validatorSettings.CountryFieldName) == 0); | |
if (countryField != null) | |
{ | |
// is the field required? | |
bool isRequired = countryField.Requred.Any(); | |
// continue handling further processing here... | |
} | |
return base.OnServerValidate(value); | |
} |
Here we’re getting an instance of the SimpleForm using WebUtil.GetParent<SimpleForm>(). From there, we’re searching for our field, also via WebUtil. One very important thing to keep in mind is that the instance of the form we’re getting is the rendered output of the form itself and not the actual form item in Sitecore content. This makes it quite difficult to determine the checked state of the required checkbox on our form field. Instead, we’re dependent upon doing an .Any() on the “Requred” property of our field to see if any RequiredWithMarkerValidator items exist.
Shout out to Mike Reynolds for the idea of running .Any() on the Requred property.
Approach 2: Obtain an Instance of the current WFFM Item
protected override bool OnServerValidate(string value) | |
{ | |
var validatorSettings = GetValidatorSettingsItem(); | |
Sitecore.Diagnostics.Assert.IsNotNull(validatorSettings, ValidationSettingsItemMissingMessage + ValidationSettingsItem); | |
// obtain an instance of the SimpleForm | |
var form = WebUtil.GetParent<SimpleForm>(this); | |
// lookup the actual form item | |
var formItem = Sitecore.Context.Database.Items[form.FormID]; | |
// obtain the WFFM field | |
var countryField = formItem.Children[validatorSettings.CountryFieldId]; | |
// get its required field value from the Checkbox field | |
Sitecore.Data.Fields.CheckboxField checkbox = countryField.Fields["Required"]; | |
if (checkbox != null) | |
{ | |
bool isRequired = checkbox.Checked; | |
// continue processing... | |
} | |
return base.OnServerValidate(value); | |
} |
Here we’re still getting an instance of the SimpleForm using WebUtilGetParent<SimpleForm>(), but now we’re using the FormId property on that control to get the Sitecore ID of the form. We can then use this to get the item in Sitecore content to read the “Required” field value and determine its checked state.
Obviously, you’ll want to do additional null checking and most likely use an ORM like Synthesis to get strongly type objects mapping back to our WFFM forms and field items.
Building a Single Page Application with AngularJS and Sitecore: Part 2
Posted: November 25, 2013 Filed under: Angular, Sitecore | Tags: Angular, Sitecore, SPA 1 CommentIn the second and final in a series of posts related to AngularJS and Sitecore, we’ll explore Angular in a Sitecore context. By running Angular within a Sitecore instance, we’re pairing the speed of Angular with dynamic rendering of Sitecore, opening our discussion to multiple interesting scenarios.
SPA Integration with Sitecore
Suppose we want more than just Sitecore content in our SPA, we also want Sitecore renderings. Our previous example ran as a SPA with a direct line to Sitecore by way of the Sitecore Item Web API. Building on the previous example, lets now have the start page of our SPA as the home item of a new Sitecore site within our instance. We’ll then define content which lets us:
- Use renderings for content outside of our views (static content for all pages across our SPA).
- Use Sitecore items as views in our Angular routing.
Note: Since we’re building off the previous example, we’re still using the Sitecore Item Web API. This could easily be changed to other methods, such as directly serializing objects to JSON.
Looking at Sitecore, this is our configuration:
- Callouts – Ads displayed below our Angular views (labeled as “Heading Callout” above).
- Profiles – Data returned by our Web API calls.
- Views – It’s here where we’ll be swapping out our Angular views for Sitecore items.
Looking at the presentation details of our views, you’ll notice they don’t have the default layout used by our SPA, as this would cause a nested default layout effect within our application. Instead, we define a “PartialView”.
The ParialView layout paired with an allprofiles rendering essentially makes this the equivalent of the allprofiles view in our previous example. The difference, however, is now our views are powered by Sitecore. We can insert other renderings, content, etc.
PartialView Layout:
The only change required is to wire everything up. We can do this rather easily by altering our routes to now point at our Sitecore views instead of html files on the file system. Since we’re in the context of a Sitecore instance, Sitecore properly handles the request serving only the content for the views requested.
function getRoutes() { | |
return [ | |
{ | |
url: '/profiles', | |
config: { | |
templateUrl: 'Views/allprofiles', | |
controller: 'allprofiles' | |
} | |
}, { | |
url: '/profiles/:profileId', | |
config: { | |
templateUrl: 'Views/modifyprofile', | |
controller: 'modifyprofile' | |
} | |
}, { | |
url: '/', | |
config: { | |
templateUrl: 'Views/main', | |
controller: 'main' | |
} | |
}]; | |
} |
Technical Considerations
Caching
Angular is fast because only specific areas of the DOM are manipulated, but we have to be aware of caching issues. Any personalized areas of content or special rendering rules may very well be ignored.
When publishing updates to our views, we want to be sure we’re getting the latest and greatest version. We can force the application to reload views by appending the item version number as a query string parameter to our view. This will force a refresh through our routing.
function getRoutes() { | |
return [ | |
{ | |
url: '/profiles', | |
config: { | |
templateUrl: 'Views/allprofiles?v=4', | |
controller: 'allprofiles' | |
} | |
}, { | |
url: '/profiles/:profileId', | |
config: { | |
templateUrl: 'Views/modifyprofile?v=6', | |
controller: 'modifyprofile' | |
} | |
}, { | |
url: '/', | |
config: { | |
templateUrl: 'Views/main?v=2', | |
controller: 'main' | |
} | |
}]; | |
} |
DMS/Tracking
Out of the box configuration, DMS will track with every page request. Since our SPA remains on the same page for each request, we’re not going to be actively tracking each action by the user. Of course, we could implement a solution where we’re firing client-side events to our DMS solution.
Page Editor
Simply won’t work. Manipulating content or rendering placement within the SPA is not possible through the page editor.
Workflows
Tests using basic workflows failed on HTML validation. The extra Angular directives caused failures upon attempting to submit for approval.
Views in Sitecore Content
Our views are publically accessible with a PartialView layout. We want to be sure these don’t make their way out to be indexed, as they don’t have a full page layout associated with them. We want to be sure these are not crawled and no pages link back to these outside the context of our SPA.
Just Because You Can, Doesn’t Mean You Should
While we all love to push the boundaries of what Sitecore can offer and building solutions while incorporating other technologies, it’s important to be aware of the pros versus the cons. Building a SPA within the context of a Sitecore instance, while an interesting discussion, may bring about more issues than problems its solving.
For me at least, if someone were to suggest a similar approach, it would have to be quite convincing. In almost all cases, all we’re going to need is access to Sitecore content. If all you need is content, you may want to take a different route (no pun intended).
Alternate Approach: Angular Views in Sitecore
If all we’re after is Sitecore renderings used in conjunction with Angular, why not simply use Angular views? Why add the complexity of a SPA, if we can achieve our goal by building dynamic forms with Angular. Using Angular views, without Angular routing, we now regain control over the issues listed above under Technical Considerations.
Conclusion
Walking through each of these posts and scenarios using Angular in a Sitecore context was fun. I encourage anyone interested in using Angular to take into account all points covered in both posts. Hopefully these experiments assist the community and other developers looking to leverage Angular in future projects.
For the complete solution, including routing, Sitecore field mappings and all views and controllers, go here: https://github.com/PetersonDave/SinglePageAppDemo/tree/with-sitecore-integrated.
Building a Single Page Application with AngularJS and Sitecore: Part 1
Posted: November 25, 2013 Filed under: Angular, Sitecore | Tags: Angular, Sitecore, SPA 1 CommentDuring the November meetup of the Philadelphia Area Sitecore User Group, we explored the possibilities of using Angular with Sitecore. Two options were explored. The first, building a SPA with Angular and Sitecore Item Web API and finally, Integrating Angular into a Sitecore instance. This post, the first in a series of two related posts, discusses the Sitecore Item Web API implementation.
SPA with Sitecore Item Web API
Building a Single Page Application where we need content from Sitecore, the Sitecore Item Web API is a perfect option. We’re able to access our Sitecore instance via GET requests, obtaining Sitecore items serialized as JSON. While adding a dependency on our SPA, we can contain the dependency as single point of entry and distribute the content across view models.
In our SPA demo site, we use an Angular factory as the entry point into our Sitecore instance by way of the Sitecore item Web API. Angular factories allow us to obtain data and reuse it across multiple controllers and routes. The data is saved in an array through get() and made available via getProfiles():
app.factory('profileservice', function ($http) { | |
var items = {}; | |
var myProfileService = {}; | |
myProfileService.addItem = function (item) { | |
items.push(item); | |
}; | |
myProfileService.removeItem = function (item) { | |
var index = items.indexOf(item); | |
items.splice(index, 1); | |
}; | |
myProfileService.getProfiles = function () { | |
return items; | |
}; | |
myProfileService.update = function (itemid, params) { | |
console.log(params); | |
var url = '-/item/v1/?sc_itemid=' + itemid; | |
$http.put(url, params); | |
}; | |
myProfileService.get = function (callback) { | |
var url = '-/item/v1/?scope=s&query=/sitecore/content/Home/Repository/*'; | |
$http.get(url) | |
.then(function (res) { | |
items = res.data.result.items; | |
callback(); | |
}); | |
}; | |
return myProfileService; | |
}); |
Note: For this demo, the get request is calling the current site, as the current site is running as the Sitecore instance. This can easily be changed to a non-relative URL path.
The factory, profileService, then serves content to our controller allprofiles. There are two methods to take note of here.:
- vm.load() – Makes the GET request to our Sitecore instance, populating the profiles array within our profileService.
- populateRepository() – Our callback method which profileService calls upon successfully loading profiles from the Sitecore Item Web API. A repository property of our model is populated here.
(function () { | |
'use strict'; | |
// Controller name is handy for logging | |
var controllerId = 'allprofiles'; | |
// Define the controller on the module. | |
// Inject the dependencies. | |
// Point to the controller definition function. | |
angular.module('app').controller(controllerId, | |
['$scope', '$http', 'profileservice', allprofiles]); | |
function allprofiles($scope, $http, profileservice) { | |
var vm = this; | |
vm.newprofile = {}; | |
vm.profileService = profileservice; | |
vm.load = function () { | |
profileservice.get(populateRepository); | |
}; | |
function populateRepository() { | |
vm.repository = profileservice.getProfiles(); | |
} | |
} | |
})(); |
Finally, within our view, we access the data and use Angular’s awesome databinding to populate our view from our view model:
<div data-ng-controller="allprofiles as vm"> | |
<p><a class="btn btn-primary btn-lg" ng-click="vm.load();">Load Data</a></p> | |
<div class="row" ng-repeat="profile in vm.repository"> | |
<div class=" col-md-1"> | |
<a class="btn btn-danger btn-small" href="#profiles/{{profile.ID}}">Edit</a> | |
</div> | |
<div class="col-md-1">{{profile.DisplayName}}</div> | |
<div class="col-md-6">{{profile.ID}}</div> | |
</div> | |
</div> |
The “Load Data” link invokes our vm.load() method, while div class “row” is repeated for each item returned in our view model’s repository property.
Technical Considerations
Security
When making Sitecore Item Web API calls, if you’re planning on making cross-domain requests, you may run into some issues. While I have seen some recommended solutions, I have not tried implementing any.
Accessing Sitecore content adds additional configuration to ensure content can be accessed by your application. In our example, we’re saving our credentials within $httpprovider’s default header. Any GET request to our Sitecore instance are accessed as our demo site’s Admin user:
app.config( | |
... | |
'$httpProvider', function($httpProvider) { | |
$httpProvider.defaults.headers.put = { 'X-Scitemwebapi-Username': 'admin' }; | |
$httpProvider.defaults.headers.put = { 'X-Scitemwebapi-Password': 'b' }; | |
}); |
Note: for obvious reasons, do not use your admin account. This is for demo purposes only.
Performance
While it might seem difficult to overload your Sitecore instance from requests coming from a SPA, be aware of how your SPA accesses your Sitecore instance. You would never want to have your method which invokes GET requests within any repeated elements, such as our ng-repeat element.
Conclusion
Accessing Sitecore content using the Sitecore Item Web API is very easy and works exceptionally well within a SPA or application using the AngularJS framework.
For the complete solution, including routing, Sitecore field mappings and all views and controllers, go here: https://github.com/PetersonDave/SinglePageAppDemo/tree/with-sitecore-webapi.
Trigger Google Analytics Events on Sitecore Web Forms for Marketers Submit Actions
Posted: October 10, 2013 Filed under: Sitecore, Web Forms for Marketers | Tags: Sitecore, Web Forms for Marketers Leave a commentSuppose we wan to record an event in Google Analytics when successfully submitting forms. Before triggering our Google Analytics event, we first need to understand the different success modes:
- Redirect – After successfully passing form validation, the request is redirected to a specified success page.
- Success Message – After successfully passing form validation, a success message is rendered on the page after the post back, keeping the user on the same page.
Whichever success mode we choose, we need a client-side event to be triggered via our Google Analytics API. One commonality between the different success modes is the rendering logic for obtaining the success message in content and rendering on our WFFM form. We’re going to take advantage of this logic to inject our client-side event at the end of the success message.
The solution involves the following steps:
- Defining our Google Analytics Tracking Save Action
- Adding the Google Analytics Tracking Save Action to a form
- Modifying the forms success action pipeline to inject the tracking script
- Let Sitecore WFFM handle the rest
Success Action
We’ll use a custom Save Action to hold our client-side script and format it to include any appropriate attributes for our tracking code.
Save actions are saved under: /sitecore/System/Modules/Web Forms for Marketers/Settings/Actions/Save Actions/
Since this is all client-side, the goal of this event is to reuse and attach the script to appropriate forms by way of a Save Action. The save action will no be doing any server-side actions. As you can see, the code for this save action is empty:
public class RecordGoogleAnalyticsSubmitAction : ISaveAction, ISubmit | |
{ | |
public void Execute(ID formid, AdaptedResultList fields, params object[] data) | |
{ | |
return; | |
} | |
public void Submit(ID formid, AdaptedResultList fields) | |
{ | |
return; | |
} | |
} |
Once created, add the save action to any forms which require tracking events on submit within Google Analytics.
public class RecordGoogleAnalyticsSubmitAction : ISaveAction, ISubmit | |
{ | |
public void Execute(ID formid, AdaptedResultList fields, params object[] data) | |
{ | |
return; | |
} | |
public void Submit(ID formid, AdaptedResultList fields) | |
{ | |
return; | |
} | |
} |
Pipeline Processor
In the WFFM config include, there are two pipeline processors for each Success Mode. We’re going to replace FormatSuccessMessage with our own to append the script to the end of the success message. The success message will be formatted for both success modes (redirect and show message).
<successAction> | |
<processor type="Sitecore.Form.Core.Pipelines.SuccessRedirect, Sitecore.Forms.Core"/> | |
<processor type="Custom.Pipelines.FormatSuccessMessage, Custom"/> | |
</successAction> |
The FormatSuccessMessage implementation is simple, we append our script to the success message:
The Google Analytics script is obtained by looking up the Save Action, if it exists on the form. Save actions are available in the SubmitSuccessArgs as XML. We’ll take that XML, parse it into a ListDefinition. From there, we can convert to items, commands and finally ActionItems. Once we find our action item in the list of ActionItems, we pull our script and format it properly.
public class FormSaveActionUtilities | |
{ | |
public static string GetGoogleAnalyticsScriptFromSaveAction(SubmitSuccessArgs args) | |
{ | |
var googleAnalyticsSubmitActionId = new ID("{617DD7D9-950B-4767-B40A-CEFA848B64C6}"); | |
var script = string.Empty; | |
var lid = ListDefinition.Parse(args.Form.SaveActions).Groups[0].ListItems; | |
if (lid == null) return script; | |
var items = lid.Select(item => args.Form.Database.GetItem(item.ItemID)); | |
var actions = items.Where(command => command != null) | |
.Select(command => new ActionItem(command)) | |
.ToList(); | |
var googleAnalyticsAction = actions.FirstOrDefault(action => action.ID == googleAnalyticsSubmitActionId); | |
if (googleAnalyticsAction != null) | |
{ | |
script = string.Format(googleAnalyticsAction.Parameters, args.Form.Name); | |
} | |
return script; | |
} | |
} |
On submit, the pipeline processor finds the script, formats it and appends it to the success message. Within the WFFM Simple Form, the form programmatically pulls our modified success message and renders within a literal, triggering our client-side tracking code.
Recent Comments