Personalizing Staticlly Based Componenets with the Sitecore Rules Engine


Someone in the SDN forums had asked how to personalize a statically placed component on a page so I thought I’d come up with a solution. One one option would of course be to use template inheritance and to personalize the component on your base page template but that’s no longer a statically placed component.

I decided to create a web control that has settings for the default rendering , datasource, and parameters but allows you to personalize the component via the rules engine.  My first attempt had me creating my own rule context and actions but after a quick rewrite I realized I didn’t need all of that and was able to just use the per-existing context and actions.

Here’s the solution.

Create a Sitecore Template with a Field and call it “Rendering Rule” and make it of type Rule and set the Datasource to “/sitecore/system/Settings/Rules/Conditional Renderings”.

Add a new one to the tree and set the rendering rule. Something easy to test is “Where the current user is adminstrator change the rendering item”. Just make sure not to try and test in preview.

Next, I created the control below which will execute the rule actions for the item

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI.WebControls;
using Sitecore.Rules.ConditionalRenderings;
using Sitecore.Data.Items;
using Sitecore.Web.UI.WebControls;
using System.Web;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Diagnostics;
using Sitecore.Rules;
using Sitecore.Data.Fields;
using Sitecore.Layouts;

namespace ConditionalRenderings
{
    public class StaticRendering : WebControl
    {
        public string DataSource { get; set; }
        public string RenderingItem { get; set; }
        public bool IsHidden { get; set; }
        public string RenderingRule { get; set; }
        public string Parameters { get; set; }

        protected override void OnLoad(EventArgs e)
        {

            Assert.IsNotNull(RenderingRule, "Rendering Rule cannot be null");
            Item RenderingRuleItem = Sitecore.Context.Database.SelectSingleItem(RenderingRule);
            Assert.IsNotNull(RenderingRuleItem, "Rendering Rule Item not found.");
            Assert.IsNotNull(RenderingItem, " Rendering Item cannot be null");
            Item renderingItem = Sitecore.Context.Database.SelectSingleItem(RenderingItem);

            //create the rendering reference and set some defaults
            RenderingReference reference = new RenderingReference(new RenderingItem(renderingItem));
            Assert.IsNotNull(RenderingItem, "Rendering Item not found");

            if (!string.IsNullOrEmpty(DataSource))
            {
                reference.Settings.DataSource = DataSource;
            }

            if (!string.IsNullOrEmpty(Parameters))
            {
                reference.Settings.Parameters = Parameters;
            }

            List references = new List();
            references.Add(reference);

            //new up the rule context and execute the rule
            ConditionalRenderingsRuleContext ruleContext = new ConditionalRenderingsRuleContext(references,reference);

            RuleList rules = RuleFactory.ParseRules(Sitecore.Context.Database, RenderingRuleItem.Fields["Rendering Rule"].GetValue(true));
            rules.Run(ruleContext);

            //make sure hide component wasn't selected as an action
            if (ruleContext.References.Count >= 1)
            {
                //add the control to the page
                this.Controls.Add(ruleContext.Reference.GetControl());
            }
            base.OnLoad(e);
        }

    }
}

Next, Add the control source to the web config under system.web/pages/controls.

<add tagPrefix=”cr” namespace=”ConditionalRenderings” assembly=”ConditionalRenderings”/>

Finally, add the control to a page pointing it at the Rendereing Rule Item we created and and default Rendering Item.

<cr:StaticRendering runat=”server” RenderingItem=”{2B556B84-364A-4AB3-A64C-933787D19619}” RenderingRule=”{11BD1B9F-A953-4F80-8692-4663B6DB4C78}” />

Package to just download and install is available here. You will still have to update the web.config to use the control.

Extending Sitecore Rocks – Creating a ContentTreeCommand and Server Component


Sitecore Rocks is completely extensible, much like Sitecore itself.  In this example I’ll be extending it to support cloning with a Sitecore Rocks Server Component and a Sitecore Explorer Command Plug-in.

The Server Component

•Server Components sit in the bin folder of the websites.
•The Hard Rocks Webservice is used for processing custom plugins.
•All assemblies must begin with Sitecore.Rocks.Server
•The class’s namespace must begin with Sitecore.Rocks.Server.Requests
•The class must have an Execute method that returns a string
•The Execute method may contain any number of string parameters

1. Create a new Sitecore Rocks Server Component Project named  Sitecore.Rocks.Server.Cloning

2. Rename the HandleRequest class to Clone in the Requests folder

3. Change the Namespace to Sitecore.Rocks.Server.Requests.Cloning

4. Use the following commented code

using System.IO;
using System.Xml;
using Sitecore.Data;
using Sitecore.Data.Items;
using System;
using Sitecore.Configuration;
namespace Sitecore.Rocks.Server.Requests.Cloning
{
    public class Clone
    {
        public string Execute(string databaseName, string sourceItemId, string targetItemId, string databaseUri)
        {
            StringWriter stringWriter = new StringWriter();
            XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter);
            xmlTextWriter.WriteStartElement("result");
            try
            {
                //clone the item
                Database database = Factory.GetDatabase(databaseName);
                Item item = database.SelectSingleItem(sourceItemId);
                Item destination = database.SelectSingleItem(targetItemId);
                Item item2 = item.CloneTo(destination);
                //write out the database uri
                xmlTextWriter.WriteStartElement("database");
                xmlTextWriter.WriteString(databaseUri);
                xmlTextWriter.WriteEndElement();
                //write out the new item id
                xmlTextWriter.WriteStartElement("item");
                xmlTextWriter.WriteString(item2.ID.ToString());
                xmlTextWriter.WriteEndElement();
            }
            catch (Exception ex)
            {
                //write out an error if we have one and Rocks will throw the error
                xmlTextWriter.WriteString("***ERROR*** " + ex.Message);
            }
            xmlTextWriter.WriteEndElement();
            return stringWriter.ToString();
        }
    }
}

5. Compile and place the assembly in the bin of your website.

Creating the Rocks Plug-in

When Rocks loads it scans the C:\Users\{user}\AppData\Local\Sitecore\Sitecore.Rocks\Plugins folder for assemblies and loads types that are marked with certain attributes[Command],[Pipeline],[FieldControl] etc…

In the code below will be doing the following :
  • Modify the constructor to set the name, group and sort order.
  • Modify the CanExecute Method to make sure only one item is selected.
  • Modify the Execute Method to Show a Dialog that allows the user to select where to place the clone and then call the Server Method.
  • Add an ExecuteCompleted  method to handle the return call from the server and refresh the screen.
  • Add a method to handle the XML Parsing.
1. Create a new Sitecore Rocks Plug-in Project and name it Sitecore.Rocks.Plugins.Cloning
2. Rename the Command to Clone
3. Replace the code with the following commented code
namespace Sitecore.Rocks.Symposium.Commands
{
    using Sitecore.VisualStudio.Commands;
    using Sitecore.VisualStudio.ContentTrees;
    using System.Linq;
    using Sitecore.VisualStudio.Data;
    using Sitecore.VisualStudio.UI.SelectItem;
    using Sitecore.VisualStudio.Extensions.IItemSelectionContextExtensions;
    using Sitecore.VisualStudio.Data.DataServices;
    using System.Xml;
    using System;
    using System.Windows;

    [Command]
    public class Clone : CommandBase
    {
        public Clone()
        {
            this.Text = "Clone";
            this.Group = "Edit";
            this.SortingValue = 2300;
        }
        public override bool CanExecute(object parameter)
        {
            //this method enables or disables the menu option
            var context = parameter as ContentTreeContext;
            if (context == null || context.SelectedItems == null || context.SelectedItems.Count() != 1)
            {
                return false;
            }
            return true;
        }

        public override void Execute(object parameter)
        {
            //this method executes when the button is pushed
            var context = parameter as ContentTreeContext;
            if (context == null || context.SelectedItems == null || context.SelectedItems.Count() != 1)
            {
                return;
            }

            //get the selected item
            IItem selectedItem = context.SelectedItems.FirstOrDefault() as IItem;

            if (selectedItem != null)
            {
                // get the root "sitecore/content" item for our select item dialog.
                var contentItemId = IdManager.GetItemId("/sitecore/content");
                var contentItemUri = new ItemUri(selectedItem.ItemUri.DatabaseUri, contentItemId);

                //Show a popup to select a target
                SelectItemDialog dialog = new SelectItemDialog();
                dialog.Initialize("Select Target Item", contentItemUri);

                if (dialog.ShowDialog() == true)
                {
                    //item is selected to clone to , send the command to the server
                    var targetItem = dialog.SelectedItem;
                    if (targetItem != null)
                    {
                        context.GetSite().DataService.ExecuteAsync("Sitecore.Rocks.Server.Requests.Cloning.Clone,Sitecore.Rocks.Server.Cloning", //The class to exectue on the server
                            completed, // the completed event
                            selectedItem.ItemUri.DatabaseName.Name,  //the database name for sitecore
                            selectedItem.ItemUri.ItemId.ToString(), //the source items id
                            targetItem.ItemId.ToString(), // the target items id
                            selectedItem.ItemUri.DatabaseUri.ToString() // the database uri to return to server
                            );
                    }
                }

            }
        }

        ExecuteCompleted completed = delegate(string response, ExecuteResult result)
        {
            //this method is fired after the item is created on the server
            if (!DataService.HandleExecute(response, result))
            {
                return;
            }
            //get the active content tree
            ContentTree activeContentTree = Sitecore.VisualStudio.UI.ActiveContext.ActiveContentTree;
            if (activeContentTree != null)
            {
                // get the return values from the xml that was returned
                string database = GetResultValue(response, "//database");
                string item = GetResultValue(response, "//item");

                //get the databaseUri in the rocks format
                DatabaseUri dbUri;
                DatabaseUri.TryParse(database, out dbUri);
                //get the itemUri in rocks format
                ItemId itemId = new ItemId(new Guid(item));
                ItemUri itemUri = new ItemUri(dbUri, itemId);
                if (itemUri != null)
                {
                    //send the user to our newly created content
                    activeContentTree.Locate(itemUri);
                }
            }
        };

        //parse xmlvalue
        public static string GetResultValue(string xml, string xPath)
        {
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(xml);
            XmlNode xmlNode = xmlDocument.SelectSingleNode("//result" + xPath);
            return xmlNode.InnerText;

        }
    }
}
4. Compile the Project.
5. Open up a new instance of visual studio and you should see your new ‘clone’ command when you browse to an item.

Federated Authentication with Sitecore and the Windows Identity Foundation


UPDATE : There is a new version of the module here:  https://marketplace.sitecore.net/en/Modules/ADFSAuthenticator.aspx

 

Over the past few months I’ve done some work integrating Sitecore with multiple Federated Authentication systems like Ping Identity, ADFS and some home grown ones.  The way Federated Authentication works is instead of logging directly into an application the application sends the user to another system for authentication. Once that system authenticates the user an encrypted token, typically SAML, is passed back to the requesting application containing credentials and other information, known as claims.

Getting everything working with the Windows Identity Foundation proved to be more challenging than some of the other API’s I worked with. I’d like to thank Owain Jones, Kern Herskind Nightingale and Ivan Sharamok for their help and suggestions that made this all work.

Here is the source and update file for this solution. There is also this sample web.config.

Step 1: Setting up your development environment for claims based authentication.

This tutorial is already written so I’m just going to point to that. I suggest you set up a basic website and get claims Authentication working there before you go and try integrating it with Sitecore.

http://www.codeproject.com/Articles/278940/Claim-based-Authentication-and-WIF-Part-2

Step 2: Add Roles to Sitecore

Login To Sitecore and create the following roles :

Extranet/Registered Users

Sitecore/Author

Assign Sitecore Author to the Sitecore Client Authoring Role so they can login to the system.

Step 3: Modify the mock STS to send the roles

After you have completed that tutorial modify the STS project and change the code in CustomSecurityTokenService.cs that writes out the claims to include two roles that exist in your Sitecore system.

protected override IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)

{

if (null == principal)

{

throw new ArgumentNullException("principal");

}

ClaimsIdentity outputIdentity = new ClaimsIdentity();

// Issue custom claims.

// TODO: Change the claims below to issue custom claims required by your application.

// Update the application's configuration file too to reflect new claims requirement.

outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name));

outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Registered Users"));

outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Author"));

return outputIdentity;

}

Step 4: Install the FedAuthenticator update package.

Login to Sitecore as an admin

Browse to /sitecore/admin/updateinstallationwizard.aspx

Install the update file found here.

Step 5 : Using Visual Studio or FedUtil.exe update the web.config of your sitecore solution to point to your STS like you did in the tutorial found under Step 1.

see : http://www.codeproject.com/Articles/278940/Claim-based-Authentication-and-WIF-Part-2

Step 6: Modify the web.config file.

Under the Modules Section move the Authentication Modules so they fire after Nexus and replace the Session Module with the one in the FedAuthenticator Module.

<add type=”Sitecore.Nexus.Web.HttpModule,Sitecore.Nexus” name=”SitecoreHttpModule” />

      <addname=“WSFederationAuthenticationModule” type=”Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35″ preCondition=”managedHandler” />

      <addname=“SessionAuthenticationModule” type=”FedAuthenticator.Authentication.WSSessionAuthenticationModule, FedAuthenticator”/>

<add type=”Sitecore.Resources.Media.UploadWatcher, Sitecore.Kernel” name=”SitecoreUploadWatcher” />

Change the Location information inside of the web.config file so it looks like the following:

</system.webServer>

<location path=”sitecore modules/Shell/FedAuthenticator/sso.aspx”>

<system.web>

<authorization>

<deny users=”?”/>

</authorization>

</system.web>

</location>

<system.web>

<!–<authorization>

<deny users=”?” />

</authorization>–>

<!– Continue to run Sitecore without script validations –>

If you now try to hit sitecore modules/Shell/FedAuthenticator/sso.aspx the user should be logged into Sitecore with the Author Role.

Step 7: Create a secured page in Sitecore.

Under the Home Item create a new Sample Item and name it ‘secured’.

Set the permissions up to break inheritance for Extranet/Everyone then grant read access and enable enheritance for the Extranet/Register Users role.

Step 8 : Test authentication to the front end of the website.

Log out of Sitecore:

Clear your cookies to remove the pre-existing token.

Browse to http://yourwebsite/secured

You should be redirected to the STS token issuer. Once you log in you will be brought back to your website and should see the secured item.

It may be helpful to add this code to your sample layout.aspx page so you can see the users information.

<div><%=Sitecore.Context.User.Name %></div>

<div>Is Virtual? <%=Sitecore.Context.User.RuntimeSettings.IsVirtual %></div>

<div>Roles:</div>

<%

foreach (var role in Sitecore.Context.User.Roles)

{

%>

<div><%=role.Name %></div>

<%

}

%>

Code Review

Ideally this solution would have had two processors and nothing else.

The TokenRequestor looks to see if access is denied to an item and sends the user to the STS with a request for a token if the user is not already authenticated.

The UserResolver looks for a token in a cookie, if it exists and the user is not authenticated we log the user in a virtual user.

That’s all I expected to need when I first wrote this but I was having trouble persisting user information.  This led us to the Sitecore support portal and we found we needed a custom authentication provider and we had to overwrite the WSSessionModule to get this to work.

The root of the problem is three fold:

  1. Both Sitecore and the Windows Identity Foundation are fighting over the threads user identity located at HttpContext.Current.Request.User.
  2. The Windows Identity Foundation does not allow you to just request and parse a token just using the API. It forces you to use the http modules. If I could do this without the modules there would be a lot less code.  This is the approach I took when integrating Ping Identity and it was much easier but my Ping Identity code only works with Ping Identity and this solution should work with a lot of other identity providers, including Ping Identity.
  3. The Sitecore Nexus API is obfuscated and I had trouble figuring out where/when the Identity was being set.

So, now here’s all the ‘other’ code that was added to get the two systems to play nice.

WSSessionAuthenticationModule was added to prevent some of the Session overriding. The binding to PostAuthenticateRequest was removed from initialize module and we override the Authenticate Request method so it exits the method if we already have a Sitecore.Context.User.

The FederatedAuthenticationProvider was also added to get the role and user information to persist. The Forms Authentication Provider uses a cookie for saving the value but this cookie wasn’t persisting when we logged in using SSO so we read the Session Token instead inside of the GetCurrentUser method of the Authentication Helper.

The login method was also changed to not write out a forms cookie.

The sso.aspx page is being used to log in to Sitecore.  The user is assigned to the roles based on the Sitecore Domain. That is why the page currently lives under the sitecore modules folder.

Things that could use improvement

  1. Currently the user is logged in on every request when a token exists. I’d like to be able to just parse the token the first time it’s sent from the STS and log the user in once but that didn’t work out.
  2. Single Sign Out is not yet implemented.
  3. This solution is currently only supporting virtual users.

Multiple Sites with One Authoring Domain in Sitecore


Sitecore allows you to manage multiple sites by binding a hostname to a root content item via the webconfig. This works fine in the delivery enviornment but for authoring you would also now need a seperate entry for each site if your site is dependent on the Sitecore.Context.Site at all.

Adding the configs settings isn’t a huge deal but if you have 50 sites you now need 50 config settings for the authoring urls, 50 for the editing urls, 100 dns entries and 50 authoring bookmarks for your authors.

I came up with the following approach to resolving the site in page editor and preview modes to help alleviate some of this pain. I can now use one authoring domain and when I push the Page Editor or Preview buttons Sitecore will resolve the site for the item.

To achieve this I did four things on top of the normal setup :

  • Added SiteResolver to resolve site based on Item if sc_itemid is in the Query String and we are in Page Editor or Preview mode.
  • Override the LinkProvider to include the sc_site in the Query String when in Page Editor or Preview Mode.
  • Turn Off Rendering.SiteResolving
  • Add A Site Entry for the “Authroing” site with the root of the home node.

Normal Setup
Root Content Items

IIS Site Bindings

Added Sites to web.config (notice host names and start items)


      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

With the setup above and I log into each domain and author the sites but I can’t author from one site and have the Site resolve correctly.
Authoring Multiple Sites from One Domain
First I add my new authoring site below my last site because I want the others to resolve first on the delivery side.

      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="authoring" hostName="" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

Now if I login to editing.training domain it resolves to this authoring site that doesn’t contain a domain name. I can log in and Edit all three of my sites but if the sites are relying on anything contained in the Sitecore.Context.Site this information will be pointing to the Authoring Site and it will be wrong.

I remembered how the cross site links resolve to the correct domain when Rendering.SiteResolving is set to true so I reflected on that code and came up with a class (mostly copy pasta’d….including the code with the Go To Label) to resolve the Site for an Item without looking at the host name.

This first class is the Link Helper Class and it’s ResolveTargetSite method is what we need.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web;
using Sitecore.IO;
using Sitecore.Sites;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Prototypes.Links
{
    public static class LinkHelper
    {
        private static Dictionary<string, SiteInfo> _siteResolvingTable;
        private static List<SiteInfo> _sites;
        private static readonly object _syncRoot = new object();
        private delegate string KeyGetter(SiteInfo siteInfo);

        public static SiteInfo ResolveTargetSite(Item item)
        {
            SiteContext site = Context.Site;
            SiteContext context2 = site;
            SiteInfo info = (context2 != null) ? context2.SiteInfo : null;
            if ((context2 != null) && item.Paths.FullPath.StartsWith(context2.StartPath))
            {
                return info;
            }
            Dictionary<string, SiteInfo> siteResolvingTable = GetSiteResolvingTable();
            string key = item.Paths.FullPath.ToLowerInvariant();
        Label_00AE:
            if (siteResolvingTable.ContainsKey(key))
            {
                return siteResolvingTable[key];
            }
            int length = key.LastIndexOf("/");
            if (length > 1)
            {
                key = key.Substring(0, length);
                goto Label_00AE;
            }
            return info;
        }

        private static Dictionary<string, SiteInfo> GetSiteResolvingTable()
        {
            List<SiteInfo> sites = SiteContextFactory.Sites;
            if (!object.ReferenceEquals(_sites, sites))
            {
                lock (_syncRoot)
                {
                    if (!object.ReferenceEquals(_sites, sites))
                    {
                        _sites = sites;
                        _siteResolvingTable = null;
                    }
                }
            }
            if (_siteResolvingTable == null)
            {
                lock (_syncRoot)
                {
                    if (_siteResolvingTable == null)
                    {
                        _siteResolvingTable = BuildSiteResolvingTable(_sites);
                    }
                }
            }
            return _siteResolvingTable;
        }

        private static Dictionary<string, SiteInfo> BuildSiteResolvingTable(IEnumerable<SiteInfo> sites)
        {
            Dictionary<string, SiteInfo> dictionary = new Dictionary<string, SiteInfo>();
            List<string> list = new List<string>();
            KeyGetter[] getterArray = new KeyGetter[] { info => FileUtil.MakePath(info.RootPath, info.StartItem).ToLowerInvariant() };
            foreach (KeyGetter getter in getterArray)
            {
                foreach (SiteInfo info in sites)
                {
                    if (!string.IsNullOrEmpty(info.HostName) && string.IsNullOrEmpty(GetTargetHostName(info)))
                    {
                        Log.Warn(string.Format("LinkBuilder. Site '{0}' should have defined 'targethostname' property in order to take participation in site resolving process.", info.Name), typeof(LinkProvider.LinkBuilder));
                    }
                    else if ((!string.IsNullOrEmpty(GetTargetHostName(info)) && !string.IsNullOrEmpty(info.RootPath)) && !string.IsNullOrEmpty(info.StartItem))
                    {
                        string key = getter(info);
                        if (!list.Exists(new Predicate<string>(key.StartsWith)))
                        {
                            dictionary.Add(key, info);
                            list.Add(key);
                        }
                    }
                }
            }
            return dictionary;
        }

        private static string GetTargetHostName(SiteInfo siteInfo)
        {
            if (!string.IsNullOrEmpty(siteInfo.TargetHostName))
            {
                return siteInfo.TargetHostName;
            }
            string hostName = siteInfo.HostName;
            if (hostName.IndexOfAny(new char[] { '*', '|' }) < 0)
            {
                return hostName;
            }
            return string.Empty;
        }

    }
}

Next I create a custom SiteResolver and place it after the normal SiteResolver in the begin request pipeline.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Web;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class SiteResolver : Sitecore.Pipelines.HttpRequest.SiteResolver
    {

        public override void Process(Pipelines.HttpRequest.HttpRequestArgs args)
        {
            if ((Sitecore.Context.PageMode.IsPreview || Sitecore.Context.PageMode.IsPageEditor) 
                && HttpContext.Current.Request.QueryString["sc_itemid"] != null)
            {
                Database db = Sitecore.Context.Database;
                if (db != null)
                {
                    Item item = db.SelectSingleItem(HttpContext.Current.Request.QueryString["sc_itemid"]);
                    if (item != null)
                    {
                        SiteInfo site = LinkHelper.ResolveTargetSite(item);
                        SiteContext siteContext = SiteContextFactory.GetSiteContext(site.Name);
                        UpdatePaths(args, siteContext);
                        Sitecore.Context.Site = siteContext;
                    }
                }
            }          
        }
    }
}

Add that to the httpRequestBeing pipeline right after the normal site resolver

        <processor type="Sitecore.Pipelines.HttpRequest.SiteResolver, Sitecore.Kernel" />
        <processor type="Sitecore.Prototypes.Links.SiteResolver, Sitecore.Prototypes.Links"/>

Now when I hit the Page Editor or Preview buttons the site is displayed using the correct Sitecore.Context.Site, but if I click on a link to another page it loses the context….

To fix this I added a custom link provider that appends the sc_site to the query string if we are in preview or pageditor mode.

Here is the class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class LinkProvider : Sitecore.Links.LinkProvider
    {

        public override string GetItemUrl(Data.Items.Item item, Sitecore.Links.UrlOptions options)
        {
            string url = base.GetItemUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public override string GetDynamicUrl(Data.Items.Item item, Sitecore.Links.LinkUrlOptions options)
        {
            string url  = base.GetDynamicUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public string AppendOption(string url, string option)
        {
            return url.Contains("?") ? url + option : url + "?" + option;
        }
    }
}

And here is where I overrode it in the config section

		<linkManager defaultProvider="authoring">
			<providers>
				<clear />
				<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false" />
        <add name="authoring" type="Sitecore.Prototypes.Links.LinkProvider, Sitecore.Prototypes.Links" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false"  />
			</providers>
		</linkManager>

Now my last problem is that because Rendering.SiteResolving is set to true so my domain name is switching so we need to set that to false in the Authoring Environment while we probably want it true on the delivery side.

       <setting name="Rendering.SiteResolving" value="false" />

Notes

  • I wouldn’t expect this to work with the Shared Source DB Site Resolver and I’m not sure about the other Shared Source Multi-Site piece.
  • Posting code in wordpress is painfull.
  • So is copying from it so I’ll try to get a download up

Auto Complete in Razor MVC with JQuery and LINQ to Entities


I had to create an auto-complete text box this week in Razor and didn’t see anything specific to doing it in Razor so I thought I’d throw this out there. I would like to say this post for getting it to work in MVC was very helpful and it didn’t take much effort to move from that to Razor and the Entity Framework.

In my example below I’ll be auto populating the product name based on the existing product names in the database using LINQ to Entities.

Step 1 : Download the JQuery Auto-Complete Library from google code

http://code.google.com/p/jquery-autocomplete/downloads/list

Step 2: Extract the following files into your scripts directory

jquery.autocomplete.js
jquery.autocomplete.css
indicator.gif

You can move them around later but for now lets keep them all in one folder and modify the css file to point to our new location for the loading gif.

background : url(‘/scripts/indicator.gif’) right center no-repeat;

Step 3 : Build your controller class

I’m selecting the top 10, distinct where the product name contains the typed in text.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MyNameSpace.Models;

namespace MyNameSpace.Controllers
{
    public class AutoCompleteController : Controller
    {
        MyNameSpaceEntities mhgDb = new MyNameSpaceEntities();

        public ActionResult ProductName(string q)
        {
            var productNames = (from p in mhgDb.Products where p.ProductName.Contains(q) select p.ProductName).Distinct().Take(10);

            string content = string.Join<string>("\n", productNames);
            return Content(content);
        }

    }
}

Step 4:  Add the JS and CSS files to your view


<script src="@Url.Content("~/Scripts/jquery.autocomplete.js")" type="text/javascript"></script>
<link href="@Url.Content("~/Scripts/jquery.autocomplete.css")" rel="stylesheet" type="text/css" />

Step 5 : Bind the Textbox to the Controller


<div class="editor-label">
@Html.LabelFor(model => model.ProductName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.ProductName)
@Html.ValidationMessageFor(model => model.ProductName)
</div>

<script type="text/javascript">

$(document).ready(function () {
$("#ProductName").autocomplete('@Url.Action("ProductName", "AutoComplete")', { minChars: 3 });
});

</script>

Step 6 : Build and Test

That’s all, there’s some tips around styling and parameters for autocomplete in the link to the other blog post I linked to above. I’d be interested in seeing a better LINQ statement if anyone has one.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MyHobbyGear.Models; 

namespace MyHobbyGear.Controllers
{
public class AutoCompleteController : Controller
{
MyHobbyGearEntities mhgDb = new MyHobbyGearEntities();
//
// GET: /AutoComplete/

public ActionResult Manufacturer(string q)
{
var manufacturers = (from p in mhgDb.Products where p.ManufacturerName.Contains(q) select p.ManufacturerName).Distinct().Take(10);

string content = string.Join<string>(“\n”,manufacturers);
return Content(content);
}

public ActionResult ProductName(string q)
{
var productNames = (from p in mhgDb.Products where p.ProductName.Contains(q) select p.ProductName).Distinct().Take(10);

string content = string.Join<string>(“\n”, productNames);
return Content(content);
}

}
}

C# Webservice for getting Link details like facebook with Open Graph Support


Was working on customizing the CK Editor for a site I’m working on and thought after using sites like facebook where they just paste a link and it auto formats it with details people are not going to want to use the actual link button. That’s too much work. I started looking around for a CK Editor Plugin that did the ‘facebook magic’ for me but I couldn’t find one. So I started looking at facebook, digging through their api and decided I needed to build a webservice to do this.

Here’s what I did:

1.  Use HtmlAgilityPack for html parsing.
2.  Get the basic info from standard ‘head’ tags.
3. Check to see if the site supports facebooks ‘open graph’
4.  If we still don’t have an image loop though all the images on the page. Download them and check they are atleast 50 pixels, ‘score’ them based on the alt tag being compared to the title and h1 tag.
5. If the link is an image itself set the link type to 2. (Link type of 0 = non html or image.)

Read more of this post

.net MVC Validation with Data Annotation


With MVC you can validate data in the UI and before sending to the database auto-magically if you add Data Annaotation Validators to your model classes. Here are some of the more commonly used validators and samples on how to use them.

Add using System.ComponentModel.DataAnnotations; to your model.

[Required] – States the field is required

sample :
[Required( AllowEmptyStrings=false,ErrorMessage=”You must eneter a name”)]
public string Name { get; set; }

[StringLength] – Used to set min and max string length.

sample:
[StringLength (20,MinimumLength=5,ErrorMessage=”Name must be between 5 and 20 characters”)]

[MaxLength] – Max length of a string

sample:
[MaxLength(20,ErrorMessage=”Name must be less than 20 Characters”);
public string Name { get; set; }

[MinLength] – minimum length of a string

sample:
[MinLength (5,ErrorMessage=”Name must be more than 5 characters”)]
public string Name { get; set; }

[Range] – States the value of a number must be between x and y.

sample:
[Range (1,99,ErrorMessage=”Age must be between 1 and 99″)]
public int age {get;set;}

[RegularExpression] – Used to validate the format of a string

sample:
[RegularExpression(“[a-z0-9!#$%&’*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&’*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?”, ErrorMessage=”Invalid Email Address”)]
public string Email { get; set; }

[PropertiesMustMatch] – Class level attribute used to validate something like Password1 = Password2.

sample:
[PropertiesMustMatch(“Password”, “ConfirmPassword”, ErrorMessage = “The password and confirmation password do not match.”)]
public class user {

public string name {get;set;}

public string Password {get;set;}

public string ConfirmPassword {get;set;}

}

Consuming JSON with Dynamics and C# 4.0


I don’t know if there’s a more standard way of doing this but I found this article that has some helper classes for consuming Json in C# : http://www.drowningintechnicaldebt.com/ShawnWeisfeld/archive/2010/08/22/using-c-4.0-and-dynamic-to-parse-json.aspx.

You can then steal consume data off of peoples Ajax applications. Course there’s no contract there and it’s not really built for public consumption so if they change the format on you it’d be kind of funny. I have seen some api’s spit out json but they tend to have a query string parameter to receive the info in xml.

That being said, it works. I don’t know if there’s a more standard way of consuming JSON in C#. If there is a more standard way I missed that link.