Quantcast
Channel: Sitecore – Brian Pedersen's Sitecore and .NET Blog
Viewing all 145 articles
Browse latest View live

Unable to serialize the session state. In ‘StateServer’ and ‘SQLServer’ mode

$
0
0

When switching the sessionState mode of your web project from InProc to SQLServer you might encounter this error:

Exception: System.Web.HttpException
Message: Unable to serialize the session state. In ‘StateServer’ and ‘SQLServer’ mode, ASP.NET will serialize the session state objects, and as a result non-serializable objects or MarshalByRef objects are not permitted. The same restriction applies if similar serialization is done by the custom session state store in ‘Custom’ mode.

.NET handles the storage of session objects differently from InProc to SQLServer. When storing session objects in a SQL Server, .NET will serialize the objects. This is necessary because the session object needs to be transferred from server to server.

To support this, all you need is to mark the objects that are part of your sessions with the [Serializable]  attribute:

namespace MyNameSpace
{
  // Class is marked as serializable
  [Serializable]
  public class MyClass
  {
    // Some code here
  }
}

So how do you find the classes that need the [Serializable] attribute?

.NET is pretty good at telling which classes needs to be serializable in the nested exception:

Nested Exception

Exception: System.Runtime.Serialization.SerializationException
Message: Type ‘MyNamespace.MyClass‘ in Assembly ‘MyAssembly, Version=4.0.4.18681, Culture=neutral, PublicKeyToken=null’ is not marked as serializable.
Source: mscorlib

MORE TO READ:

NOTE TO SITECORE DEVELOPERS:

Sitecore does not as such support other modes than InProc session state. As Sitecore states (quote from the Sitecore Scaling Guide):

The Sitecore CMS user interfaces require in-process ASP.NET session management. For CM
instances, the value of the mode attribute of the /configuration/system.web/sessionState
element in the web.config file must be InProc.
In-process session management requires you to configure the CM load balancer for server affinity —
also known as sticky sessions. You can use other values for the mode attribute in the CD
environment.

This means that the Sitecore client and page editor is not tested using other session state methods than InProc. But since the content delivery servers do not touch the Sitecore UI, it is up to you to make the front end code compatible with StateServer or SQLServer methods.

The best session state management for Sitecore CD servers would still be InProc, and let your load balancer use sticky sessions.



Sitecore 7 create item and update index at the same time

$
0
0

With Sitecore 7, the Sitecore index (based on Lucene.NET) have greatly improved, thus making the index even more usable.

One thing you should know though, is that when you add items to Sitecore using the Sitecore.Data.Items.Item.Add() method, the index is not immediately updated. The index is updated shortly after, but the time varies depending on your index update strategy.
The result is that if you query the index just after you added a new item, it is not a given that the item exists in the index.

There is a solution to this (of course, this is Sitecore). When you have added the new item, you must manually update the search index for this specific item.

I have made an Extension Method to show how this can be done:

using System;
using System.Linq;
using Lucene.Net.Index;
using Sitecore.ContentSearch;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace MyProject
{
  public static class ItemExtensions
  {
    public static Item AddAndUpdate(this Item item, string name, TemplateID templateID)
    {
      Assert.ArgumentNotNull(item, "item");
      Assert.IsTrue(item.Database.Name.ToLower() == "master", "Could not add item to " + item.Paths.FullPath + ": item is not in the 'master' database");
      Item newItem = item.Add(ItemUtil.ProposeValidItemName(name), templateID);
      Log.Audit("Item create: " + AuditFormatter.FormatItem(newItem), typeof(ItemExtensions));
      var tempItem = (SitecoreIndexableItem)newItem;
      ContentSearchManager.GetIndex("sitecore_master_index").Refresh(tempItem);
      return newItem;
    }

  }
}

The trick lies within these 2 lines of code:

var tempItem = (SitecoreIndexableItem)newItem;
ContentSearchManager.GetIndex("sitecore_master_index").Refresh(tempItem);

First the item is converted into a SitecoreIndexableItem, and then the master index is refreshed for this single item.

The extension method can be used like this:

using MyProject;

namespace MyNamespace
{
  public void AddItem(Item parent, string name, TemplateID templateID)
  {
    Item newItem = parent.AddAndUpdate(name, templateID);
  }
}

Please note that this will increase the time to create an item substantially, from barely mesureable milliseconds to a full second or so.

MORE TO READ:

 

 


URL Rewrite and Sitecore

$
0
0

The Microsoft URL Rewrite module is an ISS extension that allows you to rewrite one URL to another using regular expressions. The extension is like the Sitecore aliases on steroids, and is especially useful for mapping old dynamic URL’s to new Sitecore URL’s.
If you launch a new website on an existing domain, you can be forced to keep old API URL’s as they were, and the URL Rewrite will help you with that.

Download the URL Rewrite extension here.

Here is an example on how to use URL Rewrite Rules to map an old API URL to a new Sitecore .ashx CustomHandler.

The URL rewrite rules are easiest kept in a separate configuration file, so you need to apply the following to the system.webserver part of your web.config:

<system.webServer>    
  ...
  <rewrite>
    <rules configSource="App_Config\UrlRewriteRules.config" />
  </rewrite>
</system.webServer>

You then place the rewrite rules in a file called UrlRewriteRules.config  in the App_Config folder:

<?xml version="1.0" encoding="utf-8"?>
<rules>
  <clear />
  <rule name="ImageScaler" stopProcessing="true">
    <match url="^/scaleimage/([^/]*)/w/(\d+)/h/(\d+)/name/([a-zA-Z]+)\.([a-zA-Z]+)$" />
    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
    <action type="Rewrite" url="/Sitecore Modules/web/ImageResizer.ashx?id={R:1}&amp;width={R:2}&amp;height={R:3}&amp;name={R:4}&amp;extension={R:5}" appendQueryString="false" logRewrittenUrl="true" />
  </rule>
  ...
  ...
</rules>

The “match” regular expression determines which URL to be redirected.
In this example I try to catch /scaleimage/1200/w/800/h/600/name/scaledimage.jpg.
The “rewrite” determines the URL to rewrite to (in this case /Sitecore Modules/web/ImageResizer.ashx) and the regular expression matches are appended as parameters to the .ashx page by the {R:n} syntax.

This was the URL Rewrite part, now onto some code.

I need to register my /Sitecore Modules/web/ImageResizer.ashx in Sitecore, and I can do this by adding the following to the web.config:

</sitecore>
  <customHandlers>
    ...
    <handler trigger="/Sitecore Modules/web/ImageResizer/" handler="ImageResizer" />
  </customHandlers>
</sitecore>
...
...
<system.webserver>
  <handlers>
    ...
    <add verb="*" path="/Sitecore Modules/web/ImageResizer.ashx" type="MyCode.ImageResizer, MyCode" name="ImageResizer" />
  </handlers>
</system.webserver>

The .ashx handler is ready to be called (example code only, will not compile):

namespace MyCode
{
  public class ImageResizer : IHttpHandler
  {
    public void ProcessRequest(HttpContext context)
    {
      // Retrieving the parameters from the request as defined in the 
      // "rewrite" rule:
      // ?id={R:1}&amp;width={R:2}&amp;height={R:3}&amp;name={R:4}&amp;extension={R:5}
      string ID = context.Request.QueryString["id"];
      string width = context.Request.QueryString["width"];
      string height = context.Request.QueryString["height"];
      string name = context.Request.QueryString["name"];
      string extension = context.Request.QueryString["extension"];

      // Do some code to get the image and resizing it
      Image image = GetSomeImaginaryImageClass();

      // Stream it back to the user
      context.Response.Clear();
      context.Response.ContentType = "image/jpeg";
      context.Response.StatusCode = (int)HttpStatusCode.OK;
      context.Response.BufferOutput = true; 
      byte[] bytes = image.GetBytes();
      context.Response.OutputStream.Write(bytes, 0, bytes.Length);
      context.Response.Flush();
    }
  }
}

MORE TO READ:

 


Sitecore users custom profile properties

$
0
0

Sitecore is using the standard .net security framework. This makes it easy to setup custom profiles on users, and to add custom profile properties. To do so, do the following:

STEP 1: ADD A PROFILE TEMPLATE TO SITECORE

Go to the CORE database and create a new template containing the fields you wish to add to a profile. I have added mine at /sitecore/templates/System/Security/CustomProfile but you can put yours anywhere.

Security Template

Security Template

STEP 2: ADD A USER PROFILE BASED ON THE TEMPLATE YOU JUST CREATED

Go to the CORE database and add a new item based on the template you created, below /sitecore/system/Settings/Security/Profiles.

Custom Profile

Custom Profile

Add the new item below the standard “User” profile. Sitecore will automatically base any new user on the first profile in the list.

STEP 3: THE CODE – APPLY THE PROFILE TO A USER

In this example I have created an extranet user which profile needs to be based on my new profile. To apply the profile to my user I need to write the following code:

public void UpdateUserProfile(string userName, string profileID)
{
  Sitecore.Security.Accounts.User user = Sitecore.Security.Accounts.User.FromName(userName, true);
  user.Profile.ProfileItemId = profileID;
  user.Profile.Save();
}

// How to use the method
UpdateUserProfile("extranet\\bp", "{7BA5AA76-582D-4463-BCF3-775508C8624E}"); 

Remember to prefix the user name with the domain name (in this example, bp is a member of the extranet domain). The GUID is the ID of the profile item I created.

The profile is now based on this profile:

Profile is added to the user

Profile is added to the user

STEP 4: THE CODE – APPLY TEXT TO THE CUSTOM PROFILE

To write text on the custom profile, do the following:

public void UpdateUser(string userName, string address, string phone)
{
  Sitecore.Security.Accounts.User user = Sitecore.Security.Accounts.User.FromName(userName, true);
  user.Profile.SetCustomProperty("Address", address);
  user.Profile.SetCustomProperty("Phone", phone);
  user.Profile.Save();
}

// Calling the method:
UpdateUser("extranet\\bp", "Zepperlinerhallen, Islands Brygge 55, 2300 København S", "70 23 33 30");

The text is now added to the profile:

Text is added to the profile

Text is added to the profile

NOTES:

  • You don’t really need to create the profile in Sitecore. This is just so you can see the custom properties in the Sitecore security editor.
  • The Boolean “true” in the call to Sitecore.Security.Accounts.User.FromName(userName, true) authenticates the user, hence allowing you to edit the user and the user profile.
  • If you get the users (members) with System.Web.Security.Membership.GetAllUsers instead, the user (member) will not be authenticated, and you cannot edit the user (member, sigh…)
  • Remember to call Profile.Save(), if you don’t, the profile is not changed.

Sitecore.Web.Authentication.c__DisplayClass5.b__2(Ticket ticket) +52

$
0
0

When logging into the Sitecore client you can run into this exception:

[NullReferenceException: Object reference not set to an instance of an object.]
Sitecore.Web.Authentication.<>c__DisplayClass5.<GetExistingTicket>b__2(Ticket ticket) +52
System.Linq.Enumerable.FirstOrDefault(IEnumerable`1 source, Func`2 predicate) +168
Sitecore.Web.Authentication.TicketManager.CreateTicket(String userName, String startUrl, Boolean persist) +75
Sitecore.Pipelines.LoggedIn.Ticket.Process(LoggedInArgs args) +58

Or this exception:

[ArgumentException: Empty strings are not allowed.
Parameter name: value]
Sitecore.Web.Authentication.Ticket.set_StartUrl(String value) +255
Sitecore.Web.Authentication.Ticket.Parse(String ticket) +349
Sitecore.Web.Authentication.TicketManager.GetTickets() +319
Sitecore.Web.Authentication.TicketManager.CreateTicket(String userName, String startUrl, Boolean persist) +62
Sitecore.Pipelines.LoggedIn.Ticket.Process(LoggedInArgs args) +59

Both exception cast from the Sitecore login dialog:

[TargetInvocationException: Exception has been thrown by the target of an invocation.]
System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor) +0
System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments) +76
System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) +193
System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) +35
Sitecore.Nexus.Pipelines.NexusPipelineApi.Resume(PipelineArgs args, Pipeline pipeline) +398
Sitecore.Pipelines.Pipeline.Start(PipelineArgs args, Boolean atomic) +327
Sitecore.Pipelines.Pipeline.Start(String pipelineName, PipelineArgs args, Boolean atomic) +197
Sitecore.sitecore.login.LoginPage.Login_LoggedIn(Object sender, EventArgs e) +330
System.Web.UI.WebControls.Login.AttemptLogin() +289
System.Web.UI.WebControls.Login.OnBubbleEvent(Object source, EventArgs e) +105
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +84
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3804

The reason is that the Ticket property in the CORE database of your solution have gone pair-shaped:

Properties table from CORE database

Properties table from CORE database

This can happen if you:

  • Run update scripts (it happened for me when updating from Sitecore 7.1 to 7.2)
  • Have 2 Sitecore installations accessing the same database in:
    • Different .NET versions
    • Different Sitecore versions

To solve it, delete any SC_TICKET properties from the CORE database:

delete from [YourSitecore_Core].[dbo].[Properties]
where [key] = 'SC_TICKET';

Then restart your .NET application, and you should be fine again.

 


Disable Sitecore SPEAK dialogs

$
0
0

The new Sitecore SPEAK dialogs introduced with Sitecore 7.x are known to cause difficulties for some. Some of the issues are technical or configuration related, others are related to the fact that many users don’t like their cheese to be moved.

Sitecore SPEAK adds these dialogs by overriding the existing Sitecore dialogs. To revert them, simply disable the override.

Go to the \App_Config\Include\Sitecore.Speak.config file and you can see the overrides at the top of the file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <overrideXmlControls>
      <override xmlControl="Sitecore.Shell.Applications.Media.MediaBrowser" with="/sitecore/client/applications/Dialogs/SelectMediaDialog" />
    </overrideXmlControls>

    <overrideDialogs>
      <override dialogUrl="/sitecore/shell/Applications/Dialogs/Internal%20link.aspx" with="/sitecore/client/applications/dialogs/InsertLinkViaTreeDialog" />
      <override dialogUrl="/sitecore/shell/Applications/Dialogs/Mail%20link.aspx" with="/sitecore/client/applications/dialogs/InsertEmailDialog" />
      <override dialogUrl="/sitecore/shell/Applications/Dialogs/Anchor%20link.aspx" with="/sitecore/client/applications/dialogs/InsertAnchorDialog" />
      <override dialogUrl="/sitecore/shell/Applications/Item%20browser.aspx" with="/sitecore/client/applications/dialogs/InsertSitecoreItemViaTreeDialog" />
    </overrideDialogs>

    ...
    ...
  </sitecore>
</configuration>

Comment out the SPEAK dialogs you wish to disable, or comment out all of them if you wish to have all SPEAK dialogs to disappear.

MORE TO READ:


Improve Sitecore Membership provider performance 2-20 times

$
0
0

The Sitecore Membership provider is built on top of the default .NET membership provider. The .NET membership provider is not known to be the fastest provider available. Sure, if you have a few thousand users in your database it performs well, but if you have, say, 50.000 or even 500.000 users, the provider becomes painfully slow.

500.000 users in Sitecore

500.000 users in Sitecore

But before you start rewriting the whole thing, there is a couple of things you can do to dramatically improve performance.

I would like to thank Ulrich Kronvold, Kristian Magius, Sergey Marchenko, Ivan Sheyenjo and everyone that have been involved in getting these SQL scripts. None of the scripts are my own, but I think they are too good to keep for myself.

MODIFY STORED PROCEDURE [dbo].[aspnet_Membership_GetAllUsers] 

This script modifies the [dbo].[aspnet_Membership_GetAllUsers] stored procedure to select values based on indexed fields:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER OFF
GO

ALTER PROCEDURE [dbo].[aspnet_Membership_GetAllUsers]
    @ApplicationName       nvarchar(256),
    @PageIndex             int,
    @PageSize              int
AS
BEGIN

    DECLARE @ApplicationId uniqueidentifier
    SELECT  @ApplicationId = NULL
    SELECT  @ApplicationId = ApplicationId FROM dbo.aspnet_Applications WHERE LOWER(@ApplicationName) = LoweredApplicationName
    IF (@ApplicationId IS NULL)
        RETURN 0

    -- Set the page bounds
    DECLARE @PageLowerBound int
    DECLARE @PageUpperBound int
    DECLARE @TotalRecords   int
    SET @PageLowerBound = @PageSize * @PageIndex
    SET @PageUpperBound = @PageSize - 1 + @PageLowerBound

    -- Create a temp table TO store the select results
    /* DSZ
    CREATE TABLE #PageIndexForUsers
    (
        IndexId int IDENTITY (0, 1) NOT NULL,
        UserId uniqueidentifier
    )

    -- Insert into our temp table
    INSERT INTO #PageIndexForUsers (UserId)
    SELECT u.UserId
    FROM   dbo.aspnet_Membership m, dbo.aspnet_Users u
    WHERE  u.ApplicationId = @ApplicationId AND u.UserId = m.UserId
    ORDER BY u.UserName
    */

    SELECT @TotalRecords = COUNT(u.UserId)
    FROM dbo.aspnet_Membership m (NOLOCK), dbo.aspnet_Users u (NOLOCK)
    WHERE u.ApplicationId = @ApplicationId AND u.UserId = m.UserId

    SELECT
                u.UserName,
                m.Email,
                m.PasswordQuestion,
                m.Comment,
                m.IsApproved,
                m.CreateDate,
                m.LastLoginDate,
                u.LastActivityDate,
                m.LastPasswordChangedDate,
                u.UserId,
                m.IsLockedOut,
                m.LastLockoutDate
    FROM
                (SELECT
                        ROW_NUMBER() OVER (ORDER BY u.UserName) AS IndexId,
                        u.UserName
                FROM dbo.aspnet_Membership m (NOLOCK), dbo.aspnet_Users u (NOLOCK)
                        WHERE u.UserId = m.UserId AND u.ApplicationId = @ApplicationId AND m.ApplicationId = @ApplicationId)
                AS p INNER JOIN dbo.aspnet_Users u ON u.UserName = p.UserName INNER JOIN dbo.aspnet_Membership m ON m.UserId = u.UserId
        WHERE
                IndexId >= @PageLowerBound AND IndexId <= @PageUpperBound
    --ORDER BY UserName
    RETURN @TotalRecords
END

MODIFY STORED PROCEDURE [dbo].[aspnet_Membership_GetUserByEmail]

This script modifies the stored procedure [dbo].[aspnet_Membership_GetUserByEmail] to select fields based on indexed values:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[aspnet_Membership_GetUserByEmail]') AND type in (N'P', N'PC'))
DROP PROCEDURE [dbo].[aspnet_Membership_GetUserByEmail]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER OFF
GO

CREATE PROCEDURE [dbo].[aspnet_Membership_GetUserByEmail]
    @ApplicationName  nvarchar(256),
    @Email            nvarchar(256)
AS
BEGIN
    IF( @Email IS NULL )
        SELECT  u.UserName
        FROM    dbo.aspnet_Applications a, dbo.aspnet_Users u, dbo.aspnet_Membership m
        WHERE   LOWER(@ApplicationName) = a.LoweredApplicationName AND
                u.ApplicationId = a.ApplicationId    AND
                u.UserId = m.UserId AND
                m.LoweredEmail IS NULL
    ELSE
        SELECT  u.UserName
        FROM    dbo.aspnet_Applications a, dbo.aspnet_Users u, dbo.aspnet_Membership m
        WHERE   LOWER(@ApplicationName) = a.LoweredApplicationName AND
                u.ApplicationId = a.ApplicationId    AND
                u.UserId = m.UserId AND
                LOWER(@Email) = m.LoweredEmail AND u.ApplicationId = m.ApplicationId

    IF (@@rowcount = 0)
        RETURN(1)
    RETURN(0)
END
GO

CREATE NEW INDEX [aspnet_Membership_index]

This script creates a clustered index on fields ApplicationID, LoweredEmail and UserID used by the 2 stored procedures above:

IF  EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[aspnet_Membership]') AND name = N'aspnet_Membership_index')
DROP INDEX [aspnet_Membership_index] ON [dbo].[aspnet_Membership] WITH ( ONLINE = OFF )
GO

CREATE CLUSTERED INDEX [aspnet_Membership_index] ON [dbo].[aspnet_Membership] 
(
	[ApplicationId] ASC,
	[LoweredEmail] ASC,
	[UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
GO

CREATE NEW INDEX [aspnet_Users_Index]

This script creates a new clustered index on fields ApplicationID, LoweredUserName and UserID used by the 2 stored procedures above:

IF  EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[aspnet_Users]') AND name = N'aspnet_Users_Index')
DROP INDEX [aspnet_Users_Index] ON [dbo].[aspnet_Users] WITH ( ONLINE = OFF )
GO

CREATE UNIQUE CLUSTERED INDEX [aspnet_Users_Index] ON [dbo].[aspnet_Users] 
(
	[ApplicationId] ASC,
	[LoweredUserName] ASC,
	[UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
GO

With the changes above I managed to get at least twice the performance. Before the changes, the user manager took about 20 seconds to go to next page. After the change, it took only 2 seconds AND I could start using the search field again.

In general the performance gain was 2-5 times. The scripts does not seem to impact the stability of Sitecore, but use them with caution, as they are not official, not supported by me or Sitecore.


Sitecore and xDB – Setting up MongoDB on your developer machine

$
0
0

With Sitecore 7.5, Sitecore introduces xDB – the new MongoDB based DMS database. But before you say “oh no, yet another technology I have to deal with”, you should know that working with MongoDB is very easy. In fact it is easier than working with SQL Server.

STEP 1: DOWNLOAD MONGODB

Go to http://www.mongodb.org/downloads and download the 64 bit .zip file. Unzip the files in a directory on your computer.
I installed mine at c:\mongodb

STEP 2: SET UP THE SITECORE CONNECTIONSTRINGS.CONFIG

It is very likely that the Sitecore 7.5 (and Sitecore 8.x) connectionstrings.config are already set up correctly. If not look for the following lines, they need to point to your local machine:

<?xml version="1.0" encoding="utf-8"?>
<connectionStrings>
  <!--
    Sitecore connection strings.
    All database connections for Sitecore are configured here.
  -->
  ...
  ...
  ...
  <add name="analytics" connectionString="mongodb://localhost/analytics" />
  <add name="tracking.live" connectionString="mongodb://localhost/tracking_live" />
  <add name="tracking.history" connectionString="mongodb://localhost/tracking_history" />
  ...
</connectionStrings>

STEP 3: CREATE A .BAT FILE TO START UP MONGO

To start up mongo you need to run mongod.exe with a parameter determining where Mongo should put the database files. Create a .bat file with the following contents:

c:\MongoDB\bin\mongod.exe –dbpath “[yourfilepath]“

Replace c:\MongoDB\bin\ with the path to your Mongo files, and [yourfilepath] with the path to the folder where you would like to put your MongoDB databases.

You will need one .bat file for each of your Sitecore projects, as you would like to store a separate set of files per Sitecore.

STEP 4: START SITECORE AND ENJOY LIFE

To start up Sitecore, you run the .bat file and fire up your Sitecore and you are now running Sitecore with xDB on a local instance.

Sitecore 8 technical preview with MongoDB running

Sitecore 8 technical preview with MongoDB running

MORE TO READ:

 

 



Measuring Sitecore performance with HighResTimer

$
0
0

The HighResTimer is a Sitecore feature that has been around since Sitecore 4. It’s a simple timer that allows you to very precisely measure time in microseconds by using the underlying OS timers.

It is very easy to use:

public void MyMethod()
{
  // Initialize and start the timer:
  Sitecore.Diagnostics.HighResTimer ht = new Sitecore.Diagnostics.HighResTimer();
  ht.Start();
  try
  {
    // execute the code to be timed.
  }
  finally
  {
    // Dump the elapsed timespan to the log:
    Sitecore.Diagnostics.Log.Info("Time elapsed: " + ht.ElapsedTimeSpan.ToString(), this);
  }
}

The HighResTimer allows you to read the number of milliseconds or as I do in this example, read the time elapsed with a precision of 7 digits. The log line from the above example looks like this:

10112 12:25:28 INFO  Time elapsed: 00:00:00.0073862

More to read:


Sitecore EventQueue deadlocks – how to solve them and how to avoid them

$
0
0

The Sitecore 7.0+ EventQueue mechanism can in some rare occurrences cause a deadlock to occur.

You will notice that when it happens, your Sitecore Shell will come to a complete halt, the DMS becomes unresponsive, and you experience SQL timeout exceptions. Sitecore resumes when the deadlock is solved.

HOW TO SOLVE THEM

You will need SQL Server admin access to solve them. Open SQL Management Studio, go to the CORE database and run the following:

SELECT
db.name DBName,
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
tl.resource_type,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingTest,
tl.request_mode
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.databases db ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2
GO

When the script has run, you will get some output like this:

Deadlocks in EventQueue

Deadlocks in EventQueue

The blocking_session_id is the session id that causes the deadlock. You need to kill the blocking session. Run the kill command in the SQL Management Studio:

kill 205;
kill 62;

Voila, the deadlock is gone.

WHO CAUSED THE DEADLOCK?

Well, lets se. Run the following SQL statement:

SELECT
spid,
status,
loginame=SUBSTRING(loginame,1,40),
hostname=SUBSTRING(hostname,1, 40),
blk = CONVERT(char(3), blocked),
dbname=SUBSTRING(DB_NAME(dbid),1, 40),
cmd,
waittype

FROM master.dbo.sysprocesses
WHERE spid IN (SELECT blocked FROM master.dbo.sysprocesses)

The output reveals the sinner:

Who caused the deadlock?

Who caused the deadlock?

Oops, looks like I am the causing the deadlock myself.

HOW CAN I AVOID DEADLOCKS?

Deadlocks have a tendency to occur in multi-server environments (obviously, since the EventQueue is used to communicate between servers). They also have a tendency to occur in multi-developer envirnments, where several developers use the same SQL server.

1) Check the EventQueue size

Deadlocks also occur if your EventQueue is too large. Make sure you clean the EventQueue often and keep as little data as possible, preferably below 1000 items. The CleanupEventQueue task will help you:

<agent type="Sitecore.Tasks.CleanupEventQueue, Sitecore.Kernel" method="Run" interval="04:00:00">
    <DaysToKeep>1</DaysToKeep>
</agent>

2) Let your CM server do the publishing

Most of the EventQueue contents is publish information. If possible, your architecture should allow on the CM server to publish. The CD servers should not.

3) Have enough power in the server rack

Deadlocks occur more often on heavily loaded environments, because the insert and select statementwill be slower. Make sure you do not constantlrun out of CPU or memory on your web and sql servers.

MORE TO READ:

 

 

 

 

 


Sitecore Job Viewer – see what Sitecore is doing in the background

$
0
0

This simple .aspx page has become one of my most used tools when working with Sitecore. It’s a job viewer, displaying which jobs are running and which jobs are finished:

Job Viewer

Sitecore Job Viewer

A Sitecore job is a thread running in the background. Jobs can run in parallel or in sequence: Jobs that share the same name are queued and executed in the order they arrived.

Getting a list of all jobs are pretty simple. This returns the jobs sorted by the time they were added to the queue:

public IEnumerable<Sitecore.Jobs.Job> Jobs
{
  get
  {
    return Sitecore.Jobs.JobManager.GetJobs().OrderBy(job => job.QueueTime);
  }
}

For each job you can extract all information about the job you need. My application lists the most used data, and when you hover the mouse over one line, it displays more info, including who started the job, and all messages added to the job:

Job Details

Job Details

All of this in a simple .aspx page that can be uploaded to the /sitecore modules/shell/ folder on an ad-hoc basis to see what the heck Sitecore is doing.

The code is simple. Copy it from here, paste it into a jobs.aspx page and upload the thing to your Sitecore.

<%@ Page language="c#" EnableEventValidation="false" AutoEventWireup="true" %>

<script runat="server">

  void Page_Load(object sender, System.EventArgs e)
  {
    repJobs.DataBind();
  }

  public IEnumerable<Sitecore.Jobs.Job> Jobs
  {
    get
    {
      if (!cbShowFinished.Checked)
        return Sitecore.Jobs.JobManager.GetJobs().Where(job => job.IsDone == false).OrderBy(job => job.QueueTime);
      return Sitecore.Jobs.JobManager.GetJobs().OrderBy(job => job.QueueTime);
    }
  }

  protected string GetJobText(Sitecore.Jobs.Job job)
  {
    return string.Format("{0}\n\n{1}\n\n{2}", job.Name, job.Category, GetJobMessages(job));
  }

  protected string GetJobMessages(Sitecore.Jobs.Job job)
  {
    System.Text.StringBuilder sb = new StringBuilder();
    if (job.Options.ContextUser != null)
      sb.AppendLine("Context User: " + job.Options.ContextUser.Name);
    sb.AppendLine("Priority: " + job.Options.Priority.ToString());
    sb.AppendLine("Messages:");
    foreach (string s in job.Status.Messages)
      sb.AppendLine(s);
    return sb.ToString();
  }

  protected string GetJobColor(Sitecore.Jobs.Job job)
  {
    if (job.IsDone)
      return "#737373";
    return "#000";
  }

  protected void cbShowFinished_CheckedChanged(object sender, EventArgs e)
  {
    repJobs.DataBind();
  }
</script>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
  <head>
    <title>Job Viewer</title>
    <link href="/default.css" rel="stylesheet">
  </head>
  <body style="font-size:14px">
    <form runat="server">

      <div style="padding:10px; background-color:#efefef; border-bottom:solid 1px #aaa; border-top:solid 1px white">
        <div style="float:left; width:200px; padding-top:4px">
          <asp:CheckBox ID="cbShowFinished" runat="server" Text="Show finished jobs" Checked="false" OnCheckedChanged="cbShowFinished_CheckedChanged" AutoPostBack="true" />
        </div>
        <div style="float:right;">
          <asp:Button ID="btnRefresh" runat="server" Text="Refresh" BackColor="Green" ForeColor="White" Width="100px" Height="30px" />
        </div>
        <div style="clear:both;height:1px">&nbsp;</div>
      </div>

      <div style="padding-top:0px">
        <asp:Repeater ID="repJobs" runat="server" DataSource="<%# Jobs %>">
          <HeaderTemplate>
            <table style="width:100%">
              <thead style="background-color:#eaeaea">
                <td>Job</td>
                <td>Category</td>
                <td>Status</td>
                <td>Processed</td>
                <td>QueueTime</td>
              </thead>
          </HeaderTemplate>
          <FooterTemplate>
            </table>
          </FooterTemplate>
          <ItemTemplate>
            <tr style="background-color:beige; color:<%# GetJobColor((Container.DataItem as Sitecore.Jobs.Job)) %>" title="<%# GetJobText((Container.DataItem as Sitecore.Jobs.Job)) %>">
              <td>
                <%# Sitecore.StringUtil.Clip((Container.DataItem as Sitecore.Jobs.Job).Name, 50, true) %>
              </td>
              <td>
                <%# Sitecore.StringUtil.Clip((Container.DataItem as Sitecore.Jobs.Job).Category, 50, true) %>
              </td>
              <td>
                <%# (Container.DataItem as Sitecore.Jobs.Job).Status.State %>
              </td>
              <td>
                <%# (Container.DataItem as Sitecore.Jobs.Job).Status.Processed %> /
                <%# (Container.DataItem as Sitecore.Jobs.Job).Status.Total %>
              </td>
              <td>
                <%# (Container.DataItem as Sitecore.Jobs.Job).QueueTime.ToLocalTime() %>
              </td>
            </tr>
          </ItemTemplate>
        </asp:Repeater>
      </div>

    </form>
  </body>
</html>

MORE TO READ:


Sitecore 8 and Engagement Plans

$
0
0

With the introduction of the xDB in Sitecore 7.5, Sitecore also changed the analytics API. The VisitorManager have been replaced by the Tracker.

The Tracker is one of the base API’s for Sitecore the new Sitecore Experience Platform (the new name for Sitecore DMS, which was the new name for Sitecore OMS – do you follow me?) and the API handles the tracking of users in Sitecore.

For automatic engagement plan handling, this means that instead of using the VisitorManager to enroll users in your engagement plan, you use the Tracker and the AutomationStateManager:

using System.Linq;
using Sitecore.Analytics;
using Sitecore.Analytics.Automation.Data;
using Sitecore.Analytics.Automation.MarketingAutomation;
using Sitecore.Data.Items;

public void AddUserToEngagementPlan(string user, Item engagementPlan)
{
  Tracker.Current.Session.Identify(user);
  AutomationStateManager manager = Tracker.Current.Session.CreateAutomationStateManager();
  manager.EnrollInEngagementPlan(engagementPlan.ID, engagementPlan.Children.First().ID);
}

Parameter user is the complete username with domain (for example extranet\bp). The engagementPlan is the engagementPlan item.

The remove a user from an engagement plan you simply call RemoveFromEngagementPlan:

public void RemoveUserFromEngagementPlan(string user, Item engagementPlan)
{
  Tracker.Current.Session.Identify(user);
  AutomationStateManager manager = Tracker.Current.Session.CreateAutomationStateManager();
  manager.RemoveFromEngagementPlan(engagementPlan.ID);
}

Thanks to Alin Parjolea for the code.

MORE TO READ:


Cannot cache phrase for invariant language – Sitecore 8

$
0
0

When adding dictionary items to the CORE database in Sitecore 8, this error can occur:

Message: Cannot cache phrase for invariant language.
Source: Sitecore.Kernel
at Sitecore.Globalization.Translate.CachePhrase(String key, String phrase, Language language, DictionaryDomain domain)
at Sitecore.Globalization.ItemEventHandler.OnItemSaved(String dictionaryKey, Item item, Boolean reloadDomainCache, Boolean saveToDisk)
at … … … …

The error is related to a cached dictionary file. Sitecore caches all dictionary entries in a dictionary.dat file located in the /temp/ folder.

To solve the issue you should:

  • Delete the /temp/dictionary.dat file
  • Recycle the website

That’s it. Problem solved. Thanks to Sitecore Support for the solution.


All my Sitecore items are called __Standard Values

$
0
0

After I upgraded to Sitecore 7.5, all of my Sitecore items are called “__Standard Values“. This happens in the “Danish” language, not the “English”:

All items are called __Standard Values

All items are called __Standard Values

Don’t worry, your items have not been renamed. Sitecore have just made a tiny mistake, and added the phrase “__Standard Values” to the “Display name” of all __Standard Values:

Display Name is wrong

Display Name is wrong

  • Find the template
  • Find the __Standard Values of the template
  • Click View|Standard Fields
  • Find the field “Display name”
  • Delete the text “__Standard Values” in the field
  • Repeat for all __Standard Values that contains a text in the “Display name” field.

UPDATE:

I made an .aspx page that fixes the issue. Get the contents here.


All my Sitecore items are called __Standard Values – The fix

$
0
0

In the previous post i described how a tiny error in Sitecore made you believe that all of the Sitecore items are called __Standard Values.

The error occurs in the Danish language because Sitecore unintentionally added the word “__Standard Values” to the Display Name of several items.

A friend of mine asked for a fix. Here it is. You should:

  • Create a new .aspx page in the /sitecore modules/shell folder
  • Ensure that the website is running in the Danish (da) language
  • Copy the following contents to the .aspx page you created.
  • Call the page.

The code iterates through all Sitecore templates, identifies the __Standard Values templates where the display name is “__Standard Values” and deletes the contents in the Display Name field.

<%@ Page language="c#" EnableEventValidation="false" AutoEventWireup="true" %>

<script runat="server">

  void Page_Load(object sender, System.EventArgs e)
  {
    Response.Buffer = false;
    Response.BufferOutput = false;
    Response.Write(string.Format("{0} {1}<br/>", Sitecore.Context.ContentDatabase.Name, Sitecore.Context.Language));
    Sitecore.Data.Items.Item templateRoot = Sitecore.Context.ContentDatabase.GetItem("/sitecore/templates");
    Iterate(templateRoot);
  }

  void Iterate(Sitecore.Data.Items.Item root)
  {
    foreach (Sitecore.Data.Items.Item child in root.GetChildren())
    {
      if (child.Name == "__Standard Values" && child["__display name"] == "__Standard Values")
      {
        Response.Write(string.Format("path: {0}. Name: {1}. Display Name: {2}<br/>", child.Paths.FullPath, child.Name, child["__display name"]));
        FixDisplayName(child);
      }
      if (child.HasChildren)
        Iterate(child);
    }
  }

  void FixDisplayName(Sitecore.Data.Items.Item item)
  {
    item.Editing.BeginEdit();
    item.Fields["__display name"].Value = string.Empty;
    item.Editing.EndEdit();
  }

</script>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
  <head>
    <title>www.sitecore.net</title>
    <meta content="Microsoft Visual Studio 7.0" name="GENERATOR">
    <meta content="C#" name="CODE_LANGUAGE">
    <meta content="JavaScript" name="vs_defaultClientScript">
    <meta content="http://schemas.microsoft.com/intellisense/ie5" name="vs_targetSchema">
    <link href="/default.css" rel="stylesheet">
  </head>
  <body>
    <form runat="server">
    </form>
  </body>
</html>

As always, use at own risk, take a backup before running etc.



Sitecore Transfer items from one database to another

$
0
0

In this article I will describe how to transfer items from one Sitecore database to another. Usually you transfer content by publishing. But in rare cases this is not an option:

  • You are not moving content from master to web
  • You are not moving content to a publishing target.
  • You are moving content to another path in another database.
  • You would like to avoid raising any events that would clear cache.

Before you start transferring content, you need to know the following:

  • Transfer does not retain the path structure, because a transfer is like a copy, just between databases. If you would like to retain the path, you must do it yourself.
  • The templates of the transferred items must exist in the target database. If not, Sitecore will raise an TemplateNotFoundException.
  • Transferring is done by copying the OuterXML of an item into a string and pasting this string to another database. This could be a very CPU and memory heavy process if you choose to copy the root of a large website.

Enough talk. Here is the code:

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Proxies;
using Sitecore.Diagnostics;
using Sitecore.Exceptions;

namespace MyNameSpace
{
  public class ArchiveRepository
  {
    public void Put(Item source, Item destination, bool deep)
    {
      using (new ProxyDisabler())
      {
        ItemSerializerOptions defaultOptions = ItemSerializerOptions.GetDefaultOptions();
        defaultOptions.AllowDefaultValues = false;
        defaultOptions.AllowStandardValues = false;
        defaultOptions.ProcessChildren = deep;
        string outerXml = source.GetOuterXml(defaultOptions);
        try
        {
          destination.Paste(outerXml, false, PasteMode.Overwrite);
          Log.Audit(this, "Transfer from {0} to {1}. Deep: {2}", new[]{ AuditFormatter.FormatItem(source), AuditFormatter.FormatItem(destination), deep.ToString() });
        }
        catch (TemplateNotFoundException)
        {
          // Handle the template not found exception
        }
      }
    }

  }
}

Parameter source is the item to copy, parameter destination is the item in the destination database to copy to. Parameter deep determines if you transfer one item of the whole tree.

I created a few more methods to allow for transferring an item from one database to another whilst retaining the path structure. By adding the destination database as private property I hard-code the repository to a specific destination database.

The method Put(item) takes a source item, creates the item path if not found, then transfer the item:

    private readonly Database _database = Factory.GetDatabase("DestinationDatabase");

    public void Put(Item item)
    {
      EnsurePath(item);
      Put(item, true);
    }

    private void Put(Item item, bool deep)
    {
      Item destination = _database.GetItem(item.Parent.ID);
      Put(item, destination, deep);
    }

    private void EnsurePath(Item item)
    {
      foreach (Item ancestor in item.Axes.GetAncestors())
      {
        if (_database.GetItem(ancestor.ID) == null)
          Put(ancestor, false);
      }
    }

MORE TO READ:


Sitecore 8 ServerTimeZone – my dates are all wrong

$
0
0

This issue appeared when I upgraded from Sitecore 7.5 to Sitecore 8. Suddenly my dates on the website were 2 hours behind – on my development server it was one hour behind.

In Sitecore, my date-time was (example) 2015-04-12 00:00:00. This date was presented as 2015-04-11 22:00:00+2.

The issue arises, because Sitecore have implemented a feature that tries to handle local time zones on the server. Old Sitecore 7.5 dates are stored without any timezone information:

  • 20150412T000000

In Sitecore 8, Sitecore have changed the raw data format of the date field by taking the timezone into consideration. Which in my case means that my 2014-04-12 00:00:00 becomes 2015-04-11 22:00:00:

  • 20150411T220000Z

Notice the “Z” at the end? This implies that this is a datetime that have a timezone, and the timezone is now 2 hours behind UTC.

To fix the old datetime fields, I need to tell Sitecore which timezone non-timezone-datetime fields have. This is the setting, ServerTimeZone:

&lt;setting name=&quot;ServerTimeZone&quot; value=&quot;UTC&quot; /&gt;

I also needed to update the Lucene index, as the invalid datetime was added to the index as well.

Thanks to Peter Wind who helped find this issue.

MORE TO READ:


Sitecore 8 EXM Get the email recipient from a Sublayout

$
0
0

The Sitecore 8 Email Experience Manager (EXM) is not only about sending bulk emails. It can be used to send one-time messages like “Thank you for signing up” or “forgot your password?” directly from code.

In the previous versions of EXM, back when it was called ECM, the sublayouts of an email could get the email address of the recipient directly from the querystring parameter “ec_recipient“. But with the introduction of the xDB and the Contacts, things have complicated.

This method will return the username (not the email address) of the recipient of the email:

using System;
using System.Text;
using System.Web;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Core;
using Sitecore.Modules.EmailCampaign.Factories;
using Factory = Sitecore.Configuration.Factory;

namespace MyEXMService
{
  public class RecipientService
  {
    public string GetRecipientName(HttpRequest request)
    {
      string contactIdValue = request.QueryString[GlobalSettings.AnalyticsContactIdQueryKey];
      string itemId = Context.Request.QueryString["sc_itemid"];
      Item messageItem = Factory.GetDatabase("master").GetItem(ShortID.Parse(itemId).ToID()).Parent;
      Guid messageIdGuid = messageItem.ID.Guid;
      Guid contactId;
      using (GuidCryptoServiceProvider provider = new GuidCryptoServiceProvider(Encoding.UTF8.GetBytes(GlobalSettings.PrivateKey), messageIdGuid.ToByteArray()))
      {
        contactId = provider.Decrypt(new Guid(contactIdValue));
      }
      string contactName = EcmFactory.GetDefaultFactory().Gateways.AnalyticsGateway.GetContactIdentifier(contactId);
      return contactName;
    }
  }
}

With the username in hand, you can either look up the user in the Contacts xDB database, or in the .NET membership database:

RecipientService receipientService = new RecipientService();
string userName = receipientService.GetRecipientName(Request);
MembershipUser user = Membership.GetUser(userName);

MORE TO READ:


Extend the Sitecore FieldRenderer

$
0
0

The Sitecore FieldRenderer is the render controls used to render fields from Sitecore in a way that makes them page editable from the Page Editor (or Experience Editor as it is called in Sitecore 8).

The FieldRenderer has been known since Sitecore 5, and is still the preferred method of rendering fields when using User Controls (Sublayouts):

<%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Kernel" %>

<sc:FieldRenderer runat="server" FieldName="MyField" />
<sc:Text ID="txtText" runat="server" Field="FlowProfileEulaAndTermsSubTitle" Item="<%# this.DataSource %>"/>

The examples above shows how to use the FieldRenderer from User Controls. The sc:FieldRenderer is the base class, and the sc:Text is a text specific renderer inheriting from the FieldRenderer.

This is an example of an extended FieldRenderer I have been using for the last couple of years. The extension is used to render simple text fields such as headers, text etc. To make this easier we have added a few properties to our FieldRenderer such as:

To create a new FieldRenderer you need to inherit from Sitecore.Web.UI.WebControls.FieldControl.
The important method is protected override void DoRender(HtmlTextWriter output). Inside this method you instantiate your own FieldRenderer control, and voila, your control is Page Editable.

Here is the structure in pseudocode:

using System;
using System.ComponentModel;
using System.Web.UI;
using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web;
using Sitecore.Web.UI.WebControls;
using Sitecore.Xml.Xsl;

namespace MyFieldRendererControls
{
  [ParseChildren(false)]
  [PersistChildren(true)]
  public class FieldRendererExt : FieldControl
  {

    protected override void DoRender(HtmlTextWriter output)
    {
      string renderValue = "";
      string renderFirstPart = "";
      string renderLastPart = "";
      var fr = new FieldRenderer
        {
		  ...
		  ...
        };

      RenderFieldResult rendered = fr.RenderField();
      renderFirstPart = rendered.FirstPart;
      renderLastPart = rendered.LastPart;
      renderValue = rendered.ToString();


      output.Write(renderFirstPart);
      RenderChildren(output);
      output.Write(renderLastPart);
    }
  }
}

And here is the complete code for the enhanced FieldRenderer:

using System;
using System.ComponentModel;
using System.Web.UI;
using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web;
using Sitecore.Web.UI.WebControls;
using Sitecore.Xml.Xsl;

namespace MyFieldRendererControls
{
  [ParseChildren(false)]
  [PersistChildren(true)]
  public class TextExt : FieldControl
  {
    private string after = string.Empty;
    private string before = string.Empty;
    private string cssClass = string.Empty;
    private string cssId = string.Empty;
    private string cssStyle = string.Empty;
    private string enclosingTag = string.Empty;
    private string fieldName = string.Empty;

    [Category("Method"), Description("Always output enclosing tag regardless of it being empty or not")]
    public bool AlwaysEnclosingTag
    { get; set; }

    [Category("Method"), Description("HTML tag to wrap the field value with")]
    public string EnclosingTag
    {
      get { return enclosingTag; }
      set { enclosingTag = value; }
    }

    [Category("Method"), Description("CSS class-attribute on enclosing tag")]
    public new string CssClass
    {
      get { return cssClass; }
      set { cssClass = value; }
    }

    [Category("Method"), Description("CSS style-attribute on enclosing tag")]
    public new string CssStyle
    {
      get { return cssStyle; }
      set { cssStyle = value; }
    }

    [Category("Method"), Description("CSS id-attribute on enclosing tag - will be overridden if control ClientIDMode == Static")]
    public string CssId
    {
      get { return cssId; }
      set { cssId = value; }
    }

    [Category("Method"), Description("FieldName to be rendered from datasource")]
    public string FieldName
    {
      get { return fieldName; }
      set { fieldName = value; }
    }

    [Category("Method"), Description("Disables the page editor for the control")]
    public new bool DisableWebEditing
    { get; set; }

    [Category("Method"), Description("Put some text before")]
    public string Before
    {
      get { return before; }
      set { before = value; }
    }

    [Category("Method"), Description("Put some text after")]
    public string After
    {
      get { return after; }
      set { after = value; }
    }

    [Category("Method"), Description("Set explicit what item to process")]
    public new Item Item
    { get; set; }

    protected override void DoRender(HtmlTextWriter output)
    {
      if (string.IsNullOrEmpty(Field))
      {
        Field = FieldName;
      }

      // GET ITEM
      Item currentContextItem = null;
      try
      {
        currentContextItem = GetItem();
      }
      catch (Exception ex)
      {
        Log.Error("currentContextItem exception", ex, this);
      }

      // RENDER ITEM VALUE
      bool itemValid;
      try
      {
        itemValid = (currentContextItem != null && currentContextItem.Fields[Field] != null);
      }
      catch (Exception)
      {
        itemValid = false;
      }

      string renderValue = "";
      string renderFirstPart = "";
      string renderLastPart = "";
      if (itemValid)
      {
        var fr = new FieldRenderer
        {
          Before = Before,
          After = After,
          DisableWebEditing = DisableWebEditing,
          EnclosingTag = "",
          Item = currentContextItem,
          FieldName = Field,
          Parameters = WebUtil.BuildQueryString(GetParameters(), false)
        };

        RenderFieldResult rendered = fr.RenderField();
        renderFirstPart = rendered.FirstPart;
        renderLastPart = rendered.LastPart;
        renderValue = rendered.ToString();
      }


      // OUTPUT DATA
      if (string.IsNullOrEmpty(EnclosingTag) || (string.IsNullOrEmpty(renderValue) && (!AlwaysEnclosingTag)))
      {
        // Simple value...
        output.Write(renderFirstPart);
        RenderChildren(output);
        output.Write(renderLastPart);
      }
      else
      {
        // Otherwise...
        string attributes = "";

        if (ClientIDMode == ClientIDMode.Static)
        {
          attributes += " id='" + ID + "'";
        }
        else if (!string.IsNullOrEmpty(CssId))
        {
          attributes += " id='" + CssId + "'";
        }

        if (!string.IsNullOrEmpty(CssClass))
        {
          attributes += " class='" + CssClass + "'";
        }

        if (!string.IsNullOrEmpty(CssStyle))
        {
          attributes += " style='" + CssStyle + "'";
        }

        // Wrap it in enclosing tag and attributes
        output.Write("<" + EnclosingTag + attributes + ">");
        output.Write(renderFirstPart);
        RenderChildren(output);
        output.Write(renderLastPart);
        output.Write("</" + EnclosingTag + ">");
      }
    }

    protected override Item GetItem()
    {
      var datasource = GetDatasource();
      if (datasource != null)
      {
        return datasource;
      }

      return Item ?? base.GetItem();
    }

    protected SafeDictionary<string> GetParameters()
    {
      var parameters = new SafeDictionary<string>();

      if (Controls.Count > 0)
      {
        parameters.Add("haschildren", "true");
      }

      foreach (string key in Attributes.Keys)
      {
        string str = Attributes[key];
        parameters.Add(key, str);
      }

      return parameters;
    }

    private Item GetDatasource()
    {
      var layout = GetParent(this);

      if (layout == null)
      {
        return null;
      }

      return string.IsNullOrEmpty(layout.DataSource) ? null : Sitecore.Context.Database.GetItem(layout.DataSource);
    }

    private Sublayout GetParent(Control control)
    {
      if (control.Parent == null)
      {
        return null;
      }

      var sublayout = control.Parent as Sublayout;
      return sublayout ?? GetParent(control.Parent);
    }

  }
}

To use the new control:

<%@ Register tagPrefix="EXT" namespace="MyFieldRendererControls" assembly="MyFieldRendererControls" %>

<EXT:TextExt FieldName="NameOfField" ID="fldTitle" runat="server" EnclosingTag="h1" CssClass="class" CssStyle="color:red" />

Several of my colleagues has worked on this extension, and I would like to thank them all.

 


Sitecore 8 and Tracker.Current.Session.Identify – Overriding expired contact session lock for contact id

$
0
0

The new Sitecore 8 Experience Profile is a vital part, yes almost a cornerstone of the new xDB concept.

In xDB, you store information about the current user, anonymous or named, as a Contact in the Experience Profile (stored in the MongoDB).
Any user begins his life as an anonymous user, and the associated Contact object has no identifier, only a key matched in the cookie of the current user.

In order to connect the Contact with a named user, you use the Tracker.Current.Session.Identify(userName) method:

if (!Tracker.IsActive)
  return;
Tracker.Current.Session.Identify("extranet\\user@domain.com");

The method identifies the user@domain.com as a Contact, creates a named Contact if the contact does not exist, or merges the current contact data into one named Contact. Also, the Contact will be locked for this user.

Please note that Tracker.Current.Session.Identify(userName) accepts a string. You can create named contacts with any key. This is great if your named users does not come from Sitecore Users.

But here lies the danger. If you by accident Identifies extranet\anonymous, every anonymous visitor will be merged and locked to the same Contact. And since a contact can only be locked to one session at a time, any subsequent calls to Tracker.Current.Session.Identify will wait, and wait, and wait, … until the previous contact unlocks the Contact.

And the log will be filled with:

Message: Failed to extend contact lease for contact 43188c31-cbe9-4386-8739-c12c3dc049c2

Or even:

2844 18:01:11 ERROR Cannot finish Analytics page tracking
Exception: Sitecore.Analytics.Exceptions.ContactLockException
Message: Failed to extend contact lease for contact 43188c31-cbe9-4386-8739-c12c3dc049c2
Source: Sitecore.Analytics
at Sitecore.Analytics.Tracking.ContactManager.SaveAndReleaseContact(Contact contact)
at Sitecore.Analytics.Pipelines.EndAnalytics.ReleaseContact.Process(PipelineArgs args)
at (Object , Object[] )
at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
at Sitecore.Analytics.Pipelines.HttpRequest.EndAnalytics.Process(HttpRequestArgs args)

So it is VERY important that you check that the user identified is not your anonymous user:

// THIS IS BAD!!!
// The user could be extranet\anonymous
if (!Tracker.IsActive)
  return;
Tracker.Current.Session.Identify(Sitecore.Context.User.Name);

// THIS COULD BE A SOLUTION:
if (!Tracker.IsActive)
  return;
if (Sitecore.Current.User.Name.ToLower() == "extranet\\anonymous")
  return;
Tracker.Current.Session.Identify(Sitecore.Context.User.Name);

// OR MAYBE THIS?
if (!Tracker.IsActive)
  return;
if (!Sitecore.Context.User.IsAuthenticated)
  return;
Tracker.Current.Session.Identify(Sitecore.Context.User.Name);

MORE TO READ:


Viewing all 145 articles
Browse latest View live


Latest Images