Thursday, November 5, 2015

Fluent Assertions timeout with OutOfMemoryException when testing with Sitecore, FakeDb, and Glass

Recently, I've been trying to improve the unit testing on my Sitecore projects. Since Sitecore code is often very intimately tied to the data in the content tree, it made sense to leverage FakeDb to achieve true unit tests, rather than integration tests. Since my project is also using Glass as an ORM, this adds another wrinkle that necessitates thorough testing. Adding Fluent Assertions to my tests also provided a great way to describe the expected outcomes of my tests.

I started with a rather simple test, but started noticing that while my test was quick and returned the expected results when testing valid data, it timed out with an OutOfMemoryException if passed invalid data. On valid data tests, I was seeing less than 100 meg of memory for the testing process and very light CPU load for a few seconds. On invalid data tests, the process was grabbing almost a gig of memory and pushing the processor to more than 50% utilization for about 3-4minutes before hitting the OutOfMemoryException.



Luckily, I was able to get some hints from the stack trace, which implied the issue was happening in one of the libraries I was using, rather than my own. So I firedup the debugger and took a look. Using Visual Studio's Diagnostic Tools window, it was easy to see that my code wasn't causing the memory or timeout issues. However, I noticed that the last line of my test, the Fluent Assertions call, was where the code finally died. Here was my assertion:


Since my Fluent Assertions code was checking a FakeDb item and code that performed a GlassCast, that narrowed down where the issue might lie. Thankfully, Fluent Assertions is open source, so I cloned the version my project depended upon and started digging through the code in the debugger.

What I noticed was that Fluent Assertions uses reflection to pull all public properties of an object and then use those to output a message about the object you have have expectations about. As an experienced Sitecore developer, I'm sure you can already see where this is going.

Sitecore.Data.Items.Item is a fairly big object, with a lot of other objects as properties, including references to other Items. Since the reflection done by Fluent Assertions is recursive, we are crawling way too much to display a simple message saying why a test failed, even for objects that came from FakeDb, rather than the actual Sitecore content tree.

Fortunately, Fluent Assertions has a check in its ToString() method to determine if this reflection needs to take place. Here's the check and the reflection call:


Here we can see that the recursive reflection calls only happen if the object we are validating expectations about has not implemented a custom ToString() method.

For custom objects that just contain an Item or a Glass item as a property, you can implement your own ToString() method that pulls any relevant data out of the property, rather than letting Fluent Assertions try to pull ALL of that data via reflection.

Unfortunately, if you are using a Sitecore.Data.Items.Item or a Sitecore.FakeDb.Data.Items.ItemWrapper, you are dealing with code you didn't write, so you can't simply add a ToString() method. You also can't use an extension method, because extension methods are only used if an instance method isn't found. Since object already has a ToString() method, this prevents us from using this technique.

Luckily, we can just extend Fluent Assertions by adding our own custom Formatter that knows how to handle those two classes.


Finally, you need to register your custom formatter in your test setup.


Now if you have any Fluent Assertions based on Sitecore items, your tests will perform as expected. You can likewise expand this technique to handle your Glass items rather easily, but your implementation will vary based on the templates you used to generate your Glass items.

Fluent Assertions FormatException when trying to include a Sitecore.Data.ID in your test failure messages

Recently, when using Fluent Assertions with my unit testing of a Sitecore project, I was attempting to display a friendly message when a block of code matched a Sitecore item that did not have a specific template ID.

My assertions looked like this:


Unfortunately, when running the test, I was getting an exception, but it was from FluentAssertions, rather than my code.

If I ran the test without attaching a debugger, my exception was "Input string was not in a correct format".


If I ran the test with a debugger attached, the exception was "Index (zero based) must be greater than or equal to zero and less than the size of the argument list."


Fortunately, when I attached the debugger, I could see that the message itself was fine, but Fluent Assertions was unable to handle it. Examining the value of message revealed the likely culprit.


String.Format uses curly braces to denote substitutions. Those curly braces are coming from the Sitecore.Data.ID implemention of ToString(), which looks like this:


After checking the documentation for the different format parameters for Guid.ToString(), we can see that passing the "B" argument to Guid.ToString() is generating the curly braces. Fortunately, the default ToString() method of Guid uses the "D" parameter, rather than the "B" parameter. This will output our Guid without curly braces.

With a very minor change to how we pass the ID to Fluent Asssertions, we can bypass ID.ToString() and get a message that will work with Fluent Assertions.


After making this change, my tests began outputting messages as expected.

Monday, August 3, 2015

Sitecore 8 and MongoDB connectivity - firewall rules and connection strings

Overview


Now that MongoDB is integral to Sitecore deployments, it's important to plan for how Sitecore will connect to it. During your deployment, you'll have to account for security features, from firewall rules to authentication. This will require a basic understanding of what Sitecore needs to know to reach your MongoDB instance.

Considerations


MongoDB is an enterprise platform, and it offers a lot of features. The features leveraged by your instance will dictate how you configure Sitecore to talk to it. While many of the features of MongoDB our outside the scope of this discussion, the ones we will focus on right now include use of replica sets, authentication mechanisms such as MONGODB-CR or Kerberos, as well as security concerns like SSL or firewall rules.

Firewall Rules


This isn't a MongoDB configuration issue per se, but it is something you probably need to account for, depending on your network infrastructure and server hardening guidelines. At the simplest, MongoDB will be listening on port 27017 by default, and you'll need to make sure any firewall on the MongoDB server allows traffic to that port.

In addition, if you have your Sitecore servers isolated from the MongoDB server, the firewall between them will need to allow that traffic to flow through.

Connection Strings


If you haven't done so yet, check out the MongoDB documentation on connection strings. We're going to run through some common scenarios, but you can find out more about these options by checking that page.

Let's start by looking at your ConnectionStrings.config file, located in your Sitecore\App_Config folder. By default, your MongoDB connection string will look like this:



In production, you will probably host MongoDB on a different server than Sitecore. Whether that is an on-premise installation or a cloud based one, you'll need to tell Sitecore about it using a more specific hostname. For this example, our MongoDB instance is located on MongoData1.MyDomain.com.


You can see just how easy it was to change the hostname. While the options you are going to use here are different than what you might use for something like a SQL Server connection string, the way you pass these options will be very similar.

For brevity's sake, I'll only show one connection string from here on out and let you extrapolate how you'd update the others.

SSL

Adding support for SSL is pretty straight forward. You will need to pass the "ssl" option to the MongoDB driver.




You can see here that passing options to the MongoDB driver looks a lot like a query string. You'll use a "?" symbol to denote you are passing an option. We'll talk more about passing additional options in a moment.

Replica Sets

Now, what if your MongoDB instance was using a replica set? You need to specify the name of the replica set whenever you connect.

You'll also want to update your connection string so that if one of the hosts goes down, Sitecore, by way of the MongoDB driver, is able to talk to other members of the replica set. Note that you could send all of your traffic to one server, but then you'd lose the failover redundancy that probably influenced your decision to use a replica set to begin with.

For this example, our MongoDB instance is on a replica set named "rsSitecore". It is comprised of a primary, MongoData1.MyDomain.com, a secondary, MongoData2.MyDomain.com, and an arbiter, MongoArbiter.MyDomain.com.




You can see that we are now passing the "replicaSet" option to identify the name of our replica set. This is a required option.

Optionally, we've chose to use a comma delimited list of hostnames. You have probably noticed that we only supplied the list of data members. This was on purpose. You will not include the name of the arbiter member.

MongoDB-CR Authentication

Now let's see what happens if you turn on authentication for MongoDB. You've configured MongoDB to use it and have picked a username and password for Sitecore to use.

For this example, we're using the default authentication for MongoDB (MONGODB-CR is the default for MongoDB 2.6.10). We also have picked a username of "SCUser" and a password of "mongo!!".



Here you can see we're passing the username and password, using a ":" as the delimiter between them. You should also notice the use of a "@", which separates the username/password combo from the hostname.

A quick note about authentication here. By default, if you have not specified otherwise, when the MongoDB driver sees this connection string, it's going to assume that the user you have specified exists in the database you are trying to connect to. It is possible to have your user defined in a different database than the one you are connecting to, but that would require using the "authSource" option.

Kerberos Authentication


For Kerberos, please see my previous post, which will show an example connection string. The most important thing to remember here is that the "@" symbol is used to separate the username/password combo from the hostname. However, with Kerberos, your username will contain the "@" symbol. This is solved by escaping the "@" in the Keberos principal name.


Additional Options

When you want to add an additional key-value pair to your connection string, you won't be able to use the "&" symbol directly. Since your config file is XML, the "&" is a reserved character. You'll need to escape it.

Here's an example with a replica set named "rsSitecore" that requires SSL:


Summary

In this post we looked at some of the more common options you might see when working with Sitecore connection strings for MongoDB. Through examples, you were able to see how you might configure Sitecore for the options you are using with MongoDB and how those options can be configured simultaneously.

Tuesday, July 28, 2015

Using Kerberos to authenticate Sitecore 8 to MongoDB 2.6 on Windows Server 2012

Overview

If you are configuring Sitecore 8 to authenticate against an on-premise MongoDB instance using Kerberos, you might run into issues getting the different environments in sync. In order to get them talking successfully, you need to be aware of the different settings to configure and how small missteps can cause communication failures.

This article will discuss some of those settings and possible issues you might encounter when running MongoDB 2.6 on Windows Server. Note that as of Sitecore 8.0u3, MongoDB 3.0 is not supported.

 

Configuring Kerberos

Before worrying about getting Sitecore to talk to MongoDB, it's important to verify you have Kerberos working correctly. Running through those steps is outside the scope of this article, but let's review the basic steps for getting Kerberos working on MongoDB:
  1. Create A-Records for your MongoDB host
  2. Create a service account to run MongoDB
  3. Create a root user in the $external database
  4. Install mongod.exe as a service
  5. Create a Service Principal Name for the service
  6. Configure the mongod.exe service to run using the service account
  7. Verify connectivity using mongo.exe

Configuring Users

Case sensitivity is very important both to MongoDB and to Kerberos. You need to make sure that you maintain the casing of your user when you configure Sitecore and when you configure MongoDB.

Let's first go through the places where a user is specified, then touch on what you need to know to configure each of them.

The places where you'll specify the user are:
  • AppPoolIdentity - The identity of the Application Pool that is running Sitecore
  • AuthenticatedUser - The user MongoDB sees when you connect (this will be the same user as the AppPoolIdentity, but it WILL be case sensitive)
  • ConnectionStringUser - The user you specify in your MongoDB connection strings
  • ExternalUser - The user who was created in the $external database and who has permissions to see the Sitecore databases

 

AppPoolIdentity

You have two options for who runs the Sitecore application pool. You can either leave it as Network Service, or you can set it to run as a domain user. The option you choose will impact how you configure Sitecore to talk to MongoDB.
  • Option 1 - You run Sitecore as a domain user.
    • e.g., SCKerberos@REALM.COM
  • Option 2 - You run Sitecore as Network Service
Note: Since IIS is not case sensitive, this setting is also not case sensitive.

AuthenticatedUser  
When you talk to MongoDB, you'll be doing it as a user. This user will be based on the SSPI principal and will be the case sensitive name of the AppPoolIdentity.

In the MongoDB logs, you'll see this log entry:
  • Option 1
    • [conn1] SSPI authenticated name: SCKerberos@REALM.COM
  • Option 2
    • [conn1] SSPI authenticated name: HOSTNAME$@REALM.COM
Note: This is case sensitive and will come from Active Directory.

ConnectionStringUser

In your connection strings, you'll specify the user who has permissions to access the MongoDB databases that Sitecore uses. The value you use here should match the AppPoolIdentity user.

Although you are running Sitecore as a specific user, you still need to pass the user in your connection string. In addition, you need to tell the MongoDB driver which authentication mechanism to use to authenticate the user.

Here is a connection string from ConnectionStrings.config for connecting to the analytics database on a server named "mongo.realm.com" that is running a standalone mongo instance. Note the use of "%40" to escape the "@" symbol, which is part of a Kerberos principal name, but is reserved in the MongoDB connection string.

  • Option 1:
<add name="analytics" connectionString="mongodb://SCKerberos%40REALM.COM@mongo.realm.com/analytics?authMechanism=GSSAPI"/>
  • Option 2
<add name="analytics" connectionString="mongodb://HOSTNAME$%40REALM.COM@mongo.realm.com/analytics?authMechanism=GSSAPI"/>

In the MongoDB logs, you'll see whatever value you set there in this log entry:
  • Option 1
    • [conn1] SSPI name provided by client: SCKerberos@REALM.COM
  • Option 1
    • [conn1] SSPI name provided by client: HOSTNAME$@REALM.COM 
Note: Since this user will be checked against the $external user list, it will be case sensitive

ExternalUser

You'll grant permissions in the $external database for Sitecore on any relevant databases. This user must match the name specified in your ConnectionStringUser.

Here are the commands you might use to create your users.

  • Option 1
use $external
db.createUser(
    {
        user: "SCKerberos@REALM.COM",
        roles:
        [
            { role: "readWrite", db: "analytics" },
            { role: "readWrite", db: "tracking_live" },
            { role: "readWrite", db: "tracking_history" },
            { role: "readWrite", db: "tracking_contact" }
        ]
    }
)
  • Option 2
use $external
db.createUser(
    {
        user: "HOSTNAME$@REALM.COM",
        roles:
        [
            { role: "readWrite", db: "analytics" },
            { role: "readWrite", db: "tracking_live" },
            { role: "readWrite", db: "tracking_history" },
            { role: "readWrite", db: "tracking_contact" }
        ]
    }
)

Note: This is case sensitive

Troubleshooting

When you attempt to connect, MongoDB will recognize the connection as coming from the AuthenticatedUser. It will then compare this to the ConnectionStringUser to make sure they match. If they match, it will then look for a user in the $external database that matches the AuthenticatedUser.

The default log level for MongoDB may not give enough details to find mismatches like this. You can increase the logging detail by specifying the --setParameter logLevel=5 option when you start up mongod.exe.

Here are some common issues:
  • If your AuthenticatedUser and ConnectionStringUser do not match, you'll see a log entry like this:
    • [conn1] GSSAPI authentication failed for sckerberos@REALM.COM on $external ; AuthenticationFailed SASL(-13): authentication failure: saslServerConnAuthorize: Requested identity not authenticated identity
  • If your AuthenticatedUser does not match the AuthenticatedUser, you'll see a log entry like this:
    • [conn1] auxpropMongoDBInternal failed to find privilege document: UserNotFound Could not find user SCKerberos@REALM.COM@$external
  • If you are using a replica set, you need to make sure your connection strings include it. Here's an example of a connection to the analytics database on a replica set using Kerberos:
    • <add name="analytics" connectionString="mongodb://SCKerberos%40REALM.COM@mongo1.realm.com,mongo2.realm.com/analytics?replicaSet=rsSitecore&amp;authMechanism=GSSAPI"/>

Other Considerations

While Kerberos does use SPNs, you will not need one for the Sitecore instance. Since Sitecore will be running as a user already, the ticket it creates will be based on that identity.

If you are using replica sets, you'll also need to use a keyfile since you will be running mongod.exe with the --auth flag.


Summary 

MongoDB can use Kerberos for authentication when working with Sitecore. However, it's important to make sure you pay attention to how you configure Sitecore. Something as simple as missing case sensitivity on a user in your connection string can be difficult to track down.

Friday, May 29, 2015

MediaFramework is not importing items in production

I have been working on a custom connector for Sitecore 8.0 to Adobe Scene7 that is built on MediaFramework 2.0. Unfortunately, we ran into a snag this week when deploying to production. The process, which will slowly ingest around 30,000 images and videos into our Sitecore instance, was not pulling anything in production, even though the process has successfully been tested in both Dev and QA.

In order to track the problem down, I started adding custom logging to our Synchronizers, starting with logging every method call that was made. What quickly became apparent was that our GetMediaData method on our custom synchronizer was never getting called in production, but it was being called in QA. Using this clue, I used ILSpy to crack open the assembly for MediaFramework to figure out what processor was making this call.

In the MediaSyncItemImport.CreateItem processor, in the Process method, you can see that GetMediaData is called, but only after a successful permissions check for Context.User against the Account Settings item.



Now we are getting somewhere - we are clearly failing a permissions check. But why?

The next round of logging I added was to see which user was running my import job, since the documentation for MediaFramework contains no mention of what user it runs as, or even how to configure that user.

Since I was seeing calls to GetRootItem in production, I added a line of code there to simulate the authentication check that MediaFramework was calling, as well as to log the results. Here is the code for my GetRootItem method:

public override Item GetRootItem(object entity, Item accountItem)
{
  Log.Info(string.Format("FolderSynchronizer GetRootItem Called. Looking for root item match: {0} from accountItem {1}", Scene7Constants.RootFolderFolder, accountItem.ID), this);
           
  Item rootItem = accountItem.Children.FirstOrDefault(i => i.Name == Scene7Constants.RootFolderFolder);
  Log.Info(string.Format("FolderSynchronizer GetRootItem Result: {0}", (rootItem != null ? rootItem.Name : "null")), this);
            
  bool canCreate = AuthorizationManager.IsAllowed(rootItem, AccessRight.ItemCreate, Context.User);
  Log.Info(string.Format("Permissions check for Item:{0}, AccessRight:{1} - {2} for user {3}", rootItem.Uri, AccessRight.ItemCreate, canCreate, Context.User.Name), this);

  return rootItem;
} 

When running this in dev, we saw that the user was "sitecore\admin", but in production it was "sitecore\anonymous". That explains the permissions problem. But how should we fix this?

You could grant permissions to the anonymous user in production, but that requires editing the raw values on the item because the security UI allow you to assign permissions to this user. In addition, this felt like a sloppy workaround.

To find a better solution to our problem, I wanted to know why we were running as anonymous in production but not QA. This ended up being a simple thing to determine - our security team disabled the admin account and it needed to stay disabled. In fact, we were supposed to have disabled it in QA!

Knowing this, I decided to dig a little further and figure out how we might control the user so we could have an account that had the correct permissions for MediaFramework, but still respected a least privileges configuration.

The answer again came from ILSpy. Specifically, knowing that Agents in Sitecore run as anonymous, there had to be some code in MediaFramework that was allowing it to run as admin.

The answer comes from Sitecore.MediaFramework.Schedulers.ScopeSchedulerBase. This class has two constructors, one of which takes a "database" parameter. But that constructor invokes another and passes it a userName. Here you can see the first constructor:



A little more digging shows that ScopeSchedulerBase.Execute() calls Sitecore.Security.Accounts.UserSwitcher to set the active user to the specified user.



Now that we know how the agent runs as the admin user, we just need to tell MediaFramework to use the other constructor for the scheduler.

The answer for this is not found in the MediaFramework integration guide, but it's as simple as passing another parameter to your agent in the config file. Just below the param that sets the database, you will add a new parameter that specifies the userName. Here is what your config file might look like now.

<scheduling>
  <agent name="MediaFramework_Scene7_Import" interval="00:01:00" type="Sitecore.Media.Scene7.Schedulers.Scene7ImportScheduler, Sitecore.Media.Scene7">
 <param desc="database">master</param>
 <param desc="userName">sitecore\scene7importer</param>
 <scopeConfigurations hint="raw:AddConfiguration">
   <add ref="mediaFramework/scopeExecuteConfigurations /*[@name='Scene7_Import_Content'][1]"/>
 </scopeConfigurations>
  </agent>
</scheduling>  

Thursday, February 12, 2015

Beginning Sitecore Development, Part 3 - Adding support for Twitter Bootstrap, Sublayouts and User Controls

Overview

This is part 3 of a series of posts on Beginning Sitecore Development in Sitecore 8. Knowledge of previous posts is assumed, so if you have trouble with any of the material here, you can refer back to the previous post at Beginning Sitecore Development, Part 2 - Building a Visual Studio Project.

Goals


In this post, we're going to extend our existing solution to add styling with Twitter Bootstrap 3.32. In addition, we'll add a sublayout so our editors will have flexibility in how they layout content. Finally, we'll add some custom controls to present content in a specific format to our users.

Adding support for Bootstrap


Our existing Visual Studio project has some structure already, but it could use a little more organization to support the new additions we plan to make. 

Here's the current layout of the solution.


Let's add a vendor subfolder to assets/MySitecore so we don't have to split up any 3rd party frameworks that might have their own asset folders.

Now download Bootstrap and unzip it to the assets/Mysitecore/vendor folder. Then add the Bootstrap files to your solution by setting your project to show all files and then including the folder. Your solution should now look like this:

When you are done, your solution should look like this:


With the Bootstrap files now available, let's update our HomeLayout to reference the appropriate styles and scripts, using relative paths. Let's also make a quick update to the content of our page to verify if Bootstrap is working correctly.


With all the proper hooks in place, it's time to test it out. Build and Publish in Visual Studio. Then go into Sitecore and preview your page again. You should see it is now leveraging Bootstrap.


Creating Sublayouts for Different Page Configurations


Now that we've got Bootstrap support, it'd be really great if we could offer several different configurations of the page. Let's create some sublayouts that support different column configurations.

Let's start by updating our layout page. We'll remove all of the Bootstrap test content we created and instead add Placeholders that represent the areas of the layout where a content editor can add sublayouts, controls, or renderings. We'll also add a hook for a new style sheet.

Here's our updated HomeLayout.aspx.


Let's go ahead and create our new stylesheet we just referenced. Create mysitecore.css under the assets/MySitecore/styles folder. Here we've set it up to contain some base styles we'll use with our sublayouts.


We'll now create a sublayout that provides for a two column configuration. Add a new control to the layouts/MySitecore/Sublayouts folder in Visual Studio named TwoColumn.ascx.


Our Visual Studio project should now look like this:


Open TwoColumn.ascx and add the Bootstrap styles to divide the page into two columns. Make sure to add Sitecore Placeholder controls so the content editors can organize content they add.


Go ahead and publish your solution.

Now we need to tell Sitecore about our new Sublayout. Find the /Layout/Sublayouts node in the content tree and then create a new Sublayout Folder named MySitecore. Create another Sublayout Folder inside of that one named Sublayouts.


Right click that folder and add a new Sublayout named TwoColumn and click Next


Place it in the appropriate location.


Select the file location that corresponds to our Visual Studio project. 

Note that we have the usual chicken-and-egg problem where Sitecore wants to provision a file that we want to publish with Visual Studio. If you did the step in Sitecore first, you'd be missing the new folder structure. However, if you did the step in Visual Studio first, the file will already be there and Sitecore will be unhappy. 

Just delete the file you already published at C:\inetpub\wwwroot\[Sitecore Instance]\Website\layouts\MySitecore\Sublayouts\TwoColumn.ascx, and click Create on the Sitecore wizard. This will create the Sitecore default version of the file. Then go back into Visual Studio and republish to get our customized version.

Creating a Custom Control


In order to test our Sublayout, we need something to put in it. We'll go ahead and create a very basic user control for this purpose.

Start by adding a new Web Forms User Control to the layouts/MySitecore/Controls folder in Visual Studio named CurrentTime.ascx.

Add a label to the user control that we can store the time in. Here's our file with the markup added.


Now add the code to store the current time in the label. Here's our code behind.


Go ahead and publish your solution.

Now we need to tell Sitecore about our new control. Find the /Layout/Sublayouts/MySitecore node in the content tree and then create a new Sublayout Folder named Controls. It should look like this now.


Right click that folder and add a new Sublayout named CurrentTime and click Next.


Place it in the appropriate location.


Select the file location that corresponds to our Visual Studio project. 


Yet again, we get our chicken and egg problem. Delete the C:\inetpub\wwwroot\[Sitecore Instance]\Website\layouts\MySitecore\Controls\CurrentTime.ascx file and click Create.

Your content tree should now look like this, with a CurrentTime control and a TwoColumn sublayout.


Republish your Visual Studio solution.

Testing the SubLayout and Control


Go to the content/My Home node that corresponds to our custom page. Click on the Presentation tab in the top menu, then the Details button.

Click edit under the Default device and then click on the Controls tab to the left.

Click the Add button. Navigate to the Sublayouts/MySitecore/Sublayouts folder and select the TwoColumn sublayout. For the Placeholder text box, enter "content", which corresponds to the key value of the Placeholder we added on HomeLayout.aspx. Click Select once your screen looks like this.


Now we will add the custom control we created.

Click the Add button. Navigate to the Sublayouts/MySitecore/Controls folder and select the CurrentTime control. For the Placeholder text box, enter "/content/leftcolumn", which corresponds to the key value of the Placeholder in the TwoColumn.ascx sublayout when it exists within the "content" Placeholder of the HomeLayout.aspx layout. Click Select when your screen looks like this.


Now click OK to close the Device Editor and OK to close the Layout Details.

Save your page, then click on Publish in the top menu, then Preview. If everything went well, your page should look like this.


Since this is a two column layout, let's add a control to the other column to make sure our sublayout is behaving as we'd expect. 

Go back to the Device Editor and add another CurrentTime control, but this time, add it to "/content/rightcolumn".

Save your page, then refresh the preview. Your page should now look like this.


Extending our Solution


Now that we have a working sublayout and a basic control, we can try something a little more advanced.

Although the sample page that comes with Sitecore includes an XSL rendering, it would be more beneficial to us if that were a user control. This would allow us to leverage the rich .NET libraries rather than working only with XSL.

To start, we're going to need some new fields on our page template. Browse to the Page Template at Templates/MySitecore/Page Templates/Home Page, which we created in Part 1 of this series.

On the Builder tab, add a new Section, Page Details. Now add a Banner field that is of type Image, and a Title field that is of type Single-Line Text.

Your Page Template should now look like this. Save your changes.


Browse to the My Home item in the content tree and set the Title field. You can leave the Banner field empty for now.


Next we'll tell Sitecore about our new control. We'll follow the same process we did for adding the CurrentTime control to the content tree. Remember to add your new control to the /Layout/Sublayouts/MySitecore/Controls node in the content tree.

Name the new control "Header", then set your location to Sublayouts/MySitecore/Controls and your file location to Website/layouts/MySitecore/Controls. Click Create.

Your content tree should look like this now:


Remember to delete the file that was created by Sitecore from the file system. It should be located at C:\inetpub\wwwroot\[Sitecore Instance]\Website\layouts\MySitecore\Controls\Header.ascx

Now we'll create a new control in Visual Studio in the /layouts/MySitecore/Controls folder and name it Header.ascx. Edit the file to look like this:


If you try to compile now, you'll get an error because you are using some Sitecore controls in your new ascx. Add a reference to Sitecore.Kernel.dll, which is located in the C:\inetpub\wwwroot\[Sitecore Instance]\Website\bin folder.


Build the solution and then Publish it.

Testing the Header Control


Now we have a header control, but we need to add it to our page so we can see the results.

Go to the content/My Home node that corresponds to our custom page. Click on the Presentation tab in the top menu, then the Details button.

Click edit under the Default device and then click on the Controls tab to the left.

Click the Add button. Navigate to the Sublayouts/MySitecore/Sublayouts folder and select the TwoColumn sublayout. For the Placeholder text box, enter "pageheader", which corresponds to the key value of the Placeholder we added on HomeLayout.aspx. Click Select once your screen looks like this.


Now click OK to close the Device Editor and OK to close the Layout Details.

Save your page, then click on Publish in the top menu, then Preview. If everything went well, your page should look like this.


As a final exercise for the reader, edit the My Home page and set the Banner property. You can select the banner that is included with the sample page or upload one of your own in the Media Library. After you publish you should see that the sc:Image control you put in Header.ascx will read the Banner property on My Home and display the corresponding image on the page.

Summary

In this post, we added support for Twitter Bootstrap as our UI framework. We then added a sublayout to allow flexibility in how content is presented to users. Finally, we added two new user controls that provide custom functionality.