Improving Migration Performance

I was recently involved in a data migration project on a very limited timeline. The data would be coming from an older CRM on-premise environment and going to a new CRM Online instance. Although the data was not very complex, there was a significant amount of it and the cutover would need to happen over a single weekend.

After putting together and testing the data maps in Scribe, I was able to benchmark the full runtime; end to end migration would take 5 days. This would need to be greatly reduced in order to accommodate the 48-hour migration window I was given.

The solution I came up with involved 3 changes from the straightforward import approach.

Distributed Processing

The first involved leveraging CRM Online’s distributed computing architecture. Every operation in the Scribe migration package takes place serially, and since I’m connected using the Scribe CRM Adapter all operations are being passed through the CRM API. This triggers data validation and any business rules that are configured, which can introduce unavoidable processing delays. To get around this bottleneck I split each of my Scribe migration processes into multiple files, with source queries filtered by contiguous date ranges, and ran them simultaneously. Each copy of the Scribe Workbench establishes its own independent connection with the CRM API and distributed computing works its magic to turn my serial process into a parallel one.

Pre-Cached Lookups

The second adjustment involved cutting processing time associated with referencing related records, specifically record-owning users. Anyone who has used Scribe to push data to CRM knows that the DBLOOKUP function, while incredibly handy, can put a strain on throughput. Each lookup halts record processing while it reaches out to the CRM API and retrieves data. To avoid this extra processing step I created a table in my local SQL database and populated it with a cached copy of the user names and unique identifiers in the new CRM Online database. I then added another column to this table and filled it with the corresponding identifiers from the older on-premise CRM users. Finally, I adjusted the source queries in my migration processes to join to this new table, so that the necessary owner identifiers would be available for direct mapping within Scribe, removing the need for DBLOOKUPs entirely.

Bulk Imports and Delta Packages

The final change to my migration strategy had to do with timing. Although the previous 2 changes would make the migration jobs fit within the 48-hour window, it wouldn’t provide a lot of room for error. I determined the best approach therefore would be to perform 90% of the migration in the week that led up to the Go-Live weekend, and then wrap up the migration with a few finalization jobs to reconcile changes in the data. I began by importing all records with an Open status. This would allow for any major changes in data on a record to be brought across over the weekend without the need to re-open anything. I then made 2 copies of each of the migration packages. The first set was filtered on CRM’s ModifiedOn date, and would create or update records as necessary with any changes that occurred during the week. The second set was an update-only job that set the final statecode and statuscode. On go-live weekend these two sets were run, one after the other, in about 2 hours.

Time restraints often create an opportunity to improve processing performance. Although this solution was tailored for a specific need, the basic concepts can be applied to most migration scenarios. For assistance with accommodating your migration to Microsoft CRM, contact us today!

Do You Offer the Support Your Employees Deserve?

Many organizations feel that they offer their employees the information they need to succeed and enjoy their jobs. Unfortunately, information is often difficult to locate, spread across multiple locations or challenging to understand. Failing to provide a useful resource platform can cost your organization time, money and productivity.

Designing and developing an effective Employee Self Service, or ESS, portal or resources is vital to the continued success of your business, and the happiness of your employees.

What makes an ESS solution effective?

In order to be useful, your ESS portals and resources must be:

  • Easily accessible: Today’s employee accesses information and systems from multiple locations – the office, the road and from home. Your ESS resources must be accessible from anywhere, and must be easy to reach.
  • Consolidated: Far too often, employees are forced to access multiple systems to find answers. Creating a single repository for key documentation and resources helps your employees quickly find the information they need, without wasted time and effort bouncing from system to system.
  • Organized: An effective catalogue system and search functionality makes it easy for your employees to find the answers they need. Taking a proactive approach to organizing data will help to eliminate much of the frustration related to the search for answers.

If these characteristics don’t describe your current ESS efforts, it may be time to take action.

Make the Investment into Your Employees

A minimal investment of time and money into an effective ESS portal today will save your employees a good deal of frustration and confusion in the future. The Avtex team can help you design and build ESS portals to help employees find information relating to a wide range of issues, including:

  • Human Resources
  • IT and Technology
  • Onboarding
  • Legal
  • Compliance and Regulatory Matters
  • Sales and Marketing

Read more about our ESS services, or contact us to discuss your organization’s needs today.

 

Creating a Bootstrap Carousel in SharePoint

So you have a Bootstrapped SharePoint site. Everything’s great — until you want to add an image carousel. The default SharePoint image carousel looks like SharePoint, not Bootstrap, and is pretty bare bones.

Sure, you could put together a custom Content by Search display template. But that’s a lot of messing with the SharePoint API.

Here’s how to do it using a Content Query Web Part and some custom XSLT.

This example assumes you already have Bootstrap added to your site, and that your Bootstrap build includes the Carousel functionality.

Create a Picture Library

  1. Go to Site Contents, choose “Add an app”, and add a Picture Library.
  2. Throw in a few test pictures so we can ensure the carousel is working.

Configure a CQWP

Place a CQWP on the page and configure it:

  1. Point the CQWP at your Picture Library
  2. Name it “Image Carousel” or whatever you want.
  3. Set the custom fields so that Title uses “Title;” and Description uses “Description;”

Export the CQWP

Place a CQWP on your page, click the down-arrow in the upper right corner, and choose “Export”.

Edit the CQWP

With CQWPs, SharePoint uses a series of XSL templates to display content. There is ContentQueryMain.xsl, which handles the outer display; and there is “ItemStyle.xsl”, which handles each individual item being rendered.

The problem is that ContentQueryMain is a generic template used by all CQWPs. We need to customize that markup, but we only want it to affect the image carousel. So we have to customize this CQWP to use a custom version of both ContentQueryMain and ItemStyle.

Here’s how:

  • Open the exported CQWP in a text editor like notepad.
  • Search for a <property> tag named “MainXslLink”.  It will look like this:
    <property name="MainXslLink" type="string" />
  • Change it from self-closing to having a closing </property> tag, and then put the relative URL to your custom ContentQueryMain file inside the tag, like this:
    <property name="MainXslLink" type="string">/Style Library/XSL Style Sheets/carouselCQM.xsl</property>
  • Save the file, and upload it back to your SharePoint site.

 Create your custom ContentQueryMain

  1. Go to Style LIbrary -> XSL Style Sheets
  2. Duplicate ContentQueryMain.xsl, and name the duplicate carouselCQM.xsl
  3. Replace the entire contents with the following code:
<xsl:stylesheet
 version="1.0"
 exclude-result-prefixes="x xsl cmswrt cbq" 
 xmlns:x="http://www.w3.org/2001/XMLSchema"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:cmswrt="http://schemas.microsoft.com/WebPart/v3/Publishing/runtime"
 xmlns:cbq="urn:schemas-microsoft-com:ContentByQueryWebPart">
 <xsl:output method="xml" indent="no" media-type="text/html" omit-xml-declaration="yes"/>
 <xsl:param name="cbq_isgrouping" />
 <xsl:param name="cbq_columnwidth" />
 <xsl:param name="Group" />
 <xsl:param name="GroupType" />
 <xsl:param name="cbq_iseditmode" />
 <xsl:param name="cbq_viewemptytext" />
 <xsl:param name="cbq_errortext" />
 <xsl:param name="SiteId" />
 <xsl:param name="WebUrl" />
 <xsl:param name="PageId" />
 <xsl:param name="WebPartId" />
 <xsl:param name="FeedPageUrl" />
 <xsl:param name="FeedEnabled" />
 <xsl:param name="SiteUrl" />
 <xsl:param name="BlankTitle" />
 <xsl:param name="BlankGroup" />
 <xsl:param name="UseCopyUtil" />
 <xsl:param name="DataColumnTypes" />
 <xsl:param name="ClientId" />
 <xsl:param name="Source" />
 <xsl:param name="RootSiteRef" />
 <xsl:param name="CBQPageUrl" />
 <xsl:param name="CBQPageUrlQueryStringForFilters" />
 <xsl:param name="EffectiveDeviceChannel" />
 
 <xsl:template match="/">
 <xsl:call-template name="OuterTemplate" />
 </xsl:template>
 <xsl:template name="OuterTemplate">
 <xsl:variable name="Rows" select="/dsQueryResponse/Rows/Row" />
 <xsl:variable name="RowCount" select="count($Rows)" />
 <xsl:variable name="IsEmpty" select="$RowCount = 0" />
 <div id="{concat('cbqwp', $ClientId)}" class="cbq-layout-main">
 <xsl:if test="$cbq_iseditmode = 'True' and string-length($cbq_errortext) != 0">
 <div class="wp-content description">
 <xsl:value-of disable-output-escaping="yes" select="$cbq_errortext" />
 </div>
 </xsl:if>
 <xsl:choose>
 <xsl:when test="$IsEmpty">
 <xsl:call-template name="OuterTemplate.Empty" >
 <xsl:with-param name="EditMode" select="$cbq_iseditmode" />
 </xsl:call-template>
 </xsl:when>
 <xsl:otherwise>
 <xsl:call-template name="OuterTemplate.Body">
 <xsl:with-param name="Rows" select="$Rows" />
 <xsl:with-param name="FirstRow" select="1" />
 <xsl:with-param name="LastRow" select="$RowCount" />
 </xsl:call-template>
 </xsl:otherwise>
 </xsl:choose>
 </div>
 </xsl:template>
 
 
 <xsl:template name="OuterTemplate.Empty">
 <xsl:param name="EditMode" />
 <xsl:choose>
 <xsl:when test="$EditMode = 'True' and string-length($cbq_errortext) = 0">
 <div class="wp-content description">
 <xsl:value-of disable-output-escaping="yes" select="$cbq_viewemptytext" />
 </div>
 </xsl:when>
 <xsl:otherwise>
 <xsl:comment>empty</xsl:comment>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:template>
 
 
 
 <xsl:template name="OuterTemplate.Body">
 <xsl:param name="Rows" />
 <xsl:param name="FirstRow" />
 <xsl:param name="LastRow" />
 
 <div id="bootstrap-carousel" class="carousel slide" data-ride="carousel">
 
 <ol class="carousel-indicators">
 <xsl:for-each select="$Rows">
 <xsl:variable name="position">
 <xsl:value-of select="position()"/>
 </xsl:variable>
 <xsl:choose>
 <xsl:when test="position() = 1">
 <li class="active" data-target="#bootstrap-carousel" data-slide-to="{$position}"></li>
 </xsl:when>
 <xsl:otherwise>
 <li data-target="#bootstrap-carousel" data-slide-to="{$position}"></li>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:for-each>
 </ol>
<div class="carousel-inner" role="listbox"> 
 <xsl:for-each select="$Rows">
 <xsl:call-template name="OuterTemplate.CallItemTemplate"/>
 </xsl:for-each>
 </div>
 
 <a class="left carousel-control" href="#bootstrap-carousel" role="button" data-slide="prev"><i class="fa fa-chevron-left"> </i></a>
 <a class="right carousel-control" href="#bootstrap-carousel" role="button" data-slide="next"><i class="fa fa-chevron-right"> </i></a> 
 
 </div> 
 </xsl:template>
 
 
 <xsl:template name="OuterTemplate.CallItemTemplate">
 <xsl:choose>
 <xsl:when test="position() = 1">
 <div class="item active">
 <xsl:apply-templates select="." mode="itemstyle">
 </xsl:apply-templates>
 </div>
 </xsl:when>
 <xsl:otherwise>
 <div class="item">
 <xsl:apply-templates select="." mode="itemstyle">
 </xsl:apply-templates>
 </div>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetSafeLink">
 <xsl:param name="UrlColumnName"/>
 <xsl:if test="$UseCopyUtil = 'True'">
 <xsl:value-of select="concat($RootSiteRef, '/_layouts/15/CopyUtil.aspx?Use=id&amp;Action=dispform&amp;ItemId=',@ID,'&amp;ListId=',@ListId,'&amp;WebId=',@WebId,'&amp;SiteId=',$SiteId,'&amp;Source=',$Source)"/>
 </xsl:if>
 <xsl:if test="$UseCopyUtil != 'True'">
 <xsl:call-template name="OuterTemplate.GetSafeStaticUrl">
 <xsl:with-param name="UrlColumnName" select="$UrlColumnName"/>
 </xsl:call-template>
 </xsl:if>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetTitle">
 <xsl:param name="Title"/>
 <xsl:param name="UrlColumnName"/>
 <xsl:param name="UseFileName" select="0"/>
 <xsl:choose>
 <xsl:when test="string-length($Title) != 0 and $UseFileName = 0">
 <xsl:value-of select="$Title" />
 </xsl:when>
 <xsl:when test="$UseCopyUtil = 'True' and $UseFileName = 0">
 <xsl:value-of select="$BlankTitle" />
 </xsl:when>
 <xsl:otherwise>
 <xsl:variable name="FileNameWithExtension">
 <xsl:call-template name="OuterTemplate.GetPageNameFromUrl">
 <xsl:with-param name="UrlColumnName" select="$UrlColumnName" />
 </xsl:call-template>
 </xsl:variable>
 <xsl:choose>
 <xsl:when test="$UseFileName = 1">
 <xsl:call-template name="OuterTemplate.GetFileNameWithoutExtension">
 <xsl:with-param name="input" select="$FileNameWithExtension" />
 </xsl:call-template>
 </xsl:when>
 <xsl:otherwise>
 <xsl:value-of select="$FileNameWithExtension" />
 </xsl:otherwise>
 </xsl:choose>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:template>
 <xsl:template name="OuterTemplate.FormatColumnIntoUrl">
 <xsl:param name="UrlColumnName"/>
 <xsl:variable name="Value" select="@*[name()=$UrlColumnName]"/>
 <xsl:if test="contains($DataColumnTypes,concat(';',$UrlColumnName,',URL;'))">
 <xsl:call-template name="OuterTemplate.FormatValueIntoUrl">
 <xsl:with-param name="Value" select="$Value"/>
 </xsl:call-template>
 </xsl:if>
 <xsl:if test="not(contains($DataColumnTypes,concat(';',$UrlColumnName,',URL;')))">
 <xsl:value-of select="$Value"/>
 </xsl:if>
 </xsl:template>
 <xsl:template name="OuterTemplate.FormatValueIntoUrl">
 <xsl:param name="Value"/>
 <xsl:if test="not(contains($Value,', '))">
 <xsl:value-of select="$Value"/>
 </xsl:if>
 <xsl:if test="contains($Value,', ')">
 <xsl:call-template name="OuterTemplate.Replace">
 <xsl:with-param name="Value" select="substring-before($Value,', ')"/>
 <xsl:with-param name="Search" select="',,'"/>
 <xsl:with-param name="Replace" select="','"/>
 </xsl:call-template>
 </xsl:if>
 </xsl:template>
 <xsl:template name="OuterTemplate.Replace">
 <xsl:param name="Value"/>
 <xsl:param name="Search"/>
 <xsl:param name="Replace"/>
 <xsl:if test="contains($Value,$Search)">
 <xsl:value-of select="concat(substring-before($Value,$Search),$Replace)"/>
 <xsl:call-template name="OuterTemplate.Replace">
 <xsl:with-param name="Value" select="substring-after($Value,$Search)"/>
 <xsl:with-param name="Search" select="$Search"/>
 <xsl:with-param name="Replace" select="$Replace"/>
 </xsl:call-template>
 </xsl:if>
 <xsl:if test="not(contains($Value,$Search))">
 <xsl:value-of select="$Value"/>
 </xsl:if>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetSafeStaticUrl">
 <xsl:param name="UrlColumnName"/>
 <xsl:variable name="Url">
 <xsl:call-template name="OuterTemplate.FormatColumnIntoUrl">
 <xsl:with-param name="UrlColumnName" select="$UrlColumnName"/>
 </xsl:call-template>
 </xsl:variable>
 <xsl:value-of select="cmswrt:EnsureIsAllowedProtocol($Url)"/>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetColumnDataForUnescapedOutput">
 <xsl:param name="Name"/>
 <xsl:param name="MustBeOfType"/>
 <xsl:if test="contains($DataColumnTypes,concat(';',$Name,',',$MustBeOfType,';'))">
 <xsl:value-of select="@*[name()=$Name]"/>
 </xsl:if>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetPageNameFromUrl">
 <xsl:param name="UrlColumnName"/>
 <xsl:variable name="Url">
 <xsl:call-template name="OuterTemplate.FormatColumnIntoUrl">
 <xsl:with-param name="UrlColumnName" select="$UrlColumnName"/>
 </xsl:call-template>
 </xsl:variable>
 <xsl:call-template name="OuterTemplate.GetPageNameFromUrlRecursive">
 <xsl:with-param name="Url" select="$Url"/>
 </xsl:call-template>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetPageNameFromUrlRecursive">
 <xsl:param name="Url"/>
 <xsl:choose>
 <xsl:when test="contains($Url,'/') and substring($Url,string-length($Url)) != '/'">
 <xsl:call-template name="OuterTemplate.GetPageNameFromUrlRecursive">
 <xsl:with-param name="Url" select="substring-after($Url,'/')"/>
 </xsl:call-template>
 </xsl:when>
 <xsl:otherwise>
 <xsl:value-of select="$Url"/>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetGroupName">
 <xsl:param name="GroupName"/>
 <xsl:param name="GroupType"/>
 <xsl:choose>
 <xsl:when test="string-length(normalize-space($GroupName)) = 0">
 <xsl:value-of select="$BlankGroup"/>
 </xsl:when>
 <xsl:otherwise>
 <xsl:choose>
 <xsl:when test="$GroupType='URL'">
 <xsl:variable name="Url">
 <xsl:call-template name="OuterTemplate.FormatValueIntoUrl">
 <xsl:with-param name="Value" select="$GroupName"/>
 </xsl:call-template>
 </xsl:variable>
 <xsl:call-template name="OuterTemplate.GetPageNameFromUrlRecursive">
 <xsl:with-param name="Url" select="$Url"/>
 </xsl:call-template>
 </xsl:when>
 <xsl:otherwise>
 <xsl:value-of select="$GroupName" />
 </xsl:otherwise>
 </xsl:choose>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:template>
 <xsl:template name="OuterTemplate.CallPresenceStatusIconTemplate">
 <xsl:if test="string-length(@SipAddress) != 0">
 <span class="ms-imnSpan">
 <a href="#" onclick="IMNImageOnClick(event);return false;" class="ms-imnlink">
 <span class="ms-spimn-presenceWrapper ms-imnImg ms-spimn-imgSize-10x10">
 <img src="/_layouts/15/images/spimn.png?rev=41" class="ms-spimn-img ms-spimn-presence-disconnected-10x10x32" onload="IMNRC('{@SipAddress}')" ShowOfflinePawn="1" alt="" id="{concat('MWP_pawn_',$ClientId,'_',@ID,'type=sip')}"/>
 </span>
 </a>
 </span>
 </xsl:if>
 </xsl:template>
 <xsl:template name="OuterTemplate.GetFileNameWithoutExtension">
 <xsl:param name="input"/>
 <xsl:variable name="extension">
 <xsl:value-of select="substring-after($input, '.')"/>
 </xsl:variable>
 <xsl:choose>
 <xsl:when test="contains($extension, '.')">
 <xsl:variable name="afterextension">
 <xsl:call-template name="OuterTemplate.GetFileNameWithoutExtension">
 <xsl:with-param name="input" select="$extension"/>
 </xsl:call-template>
 </xsl:variable>
 <xsl:value-of select="concat(substring-before($input, '.'), $afterextension)"/>
 </xsl:when>
 <xsl:otherwise>
 <xsl:choose>
 <xsl:when test="contains($input, '.')">
 <xsl:value-of select="substring-before($input, '.')"/>
 </xsl:when>
 <xsl:otherwise>
 <xsl:value-of select="$input"/>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:otherwise>
 </xsl:choose>
 </xsl:template>
 </xsl:stylesheet>

This template does one thing: creates the outer wrapper markup for the slideshow, then calls the ItemStyle template to render out each individual slide.

Create a custom Item template

  1. Open ItemStyle.xsl
  2. Add the following code to the bottom of it:
<!-- Image carousel -->
 <xsl:template name="ImageGallery" match="Row[@Style='ImageGallery']" mode="itemstyle">
 <xsl:variable name="SafeLinkUrl">
 <xsl:call-template name="OuterTemplate.GetSafeLink">
 <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
 </xsl:call-template>
 </xsl:variable>
 <img src="{$SafeLinkUrl}" style="max-width:100% !important; width:100% !important;" />
 
 <div class="carousel-caption">
 <h3><xsl:value-of select="@Title"/></h3>
 <xsl:value-of select="@Description" />
 </div>
 </xsl:template>

This handles the display for each slide — the slide itself, as well as the Title and Description, if present.

Set your Image Carousel CQWP to use  the new XSL templates

  1. Edit your Image Carousel web part on the SharePoint page
  2. Under “Presentation”, set the Group Style to “Default” and the Item Style to “ImageGallery”.

You’re done! Your carousel should now be functional.