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.

Advertisements

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.

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);
}

}
}

.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;}

}

Rich Text Editing with CKEditor, MVC 3 and the Razor Engine


I was creating a model /view for editing large chunks of text on my website with MVC and the Razor engine. I created everything following Sott Gu’s tutorial on how to build a music store. When all was said and done I had a 50 character wide single line textbox for editing a large html block and thought ok this isn’t I deal.  Here are the steps I took to get a rich text editor in there.

Most of this came from this post on code project but it’s a bit outdated and doesn’t have everything.

The FCK Editor became the CK Editor, I guess they opted to drop the F over adding a U.

Step 1  : Change to Text Area

You should  have something like this in your Create and Edit Views

@Html.EditorFor(model => model.Text)

Lets change that to :

@Html.TextAreaFor(model => model.Text, new { @class = “adminRichText” })

Step 2 : Widen the text area

In your Site.Css file add the folowing to widen the text area :

.adminRichText
{
width:450px;
}

Step 3 : Add the CK Editor (formerly FCK Editor) to your project

a. Download here

b. Create a ‘Javascript’ folder under ‘Content’ in your website.

c. Extract to /Javascript/Content.

Step 4 : Include the scripts in your View

Below the validation scripts add

<script type=”text/javascript” src=”@Url.Content(“~/content/javascript/ckeditor/ckeditor.js”)”></script>

<script type=”text/javascript” src=”@Url.Content(“~/content/javascript/ckeditor/adapters/jquery.js”)”></script>

Step 5 : Turn the text area into a Rich Text Box

At bottom of view after controls are initialized add:

<script language=”javascript” type=”text/javascript”>
jQuery(“.adminRichText”).ckeditor();
</script>

Step 6 : Enable Post Back of Rich Text for your Edit and Create Methods in your controller class

[HttpPost, ValidateInput(false)]
public ActionResult Create(AreaText areaText)

[HttpPost, ValidateInput(false)]
public ActionResult Edit(string id, FormCollection collection)

Step 7: Use Html.Raw to display the data

On your index view update the output area to decode the html.

item.Text

becomes

@Html.Raw(item.Text)

ckEditor in mvc

ckEditor in mvc

Note : There are some good suggestions in the comments on ways to continue using validation :

Option 1 from Micheal.
“In your CKEditor config file add the following line:
config.htmlEncodeOutput = true;”

Option 2 from Blair.
“I was able to get this to work without having to turn off validation by putting in
[AllowHtml] in the property definition”

Internal linking with a C# HttpModule


I’m still not sure all this relative vs absolute path internal linking stuff I’m reading about in SEO ins’t due to the fact there’s confusion between what an absolute link is vs a link with a fully qualified domain but here’s a http module that will include the fully qualified domain with protocol for all of your internal links.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Text;

namespace MvcApplication1
{
     public class AbsolutePathRewriter : IHttpModule
    {
        HttpApplication application;
        #region IHttpModule Members
        public void Dispose()
        {
            //Empty
        }

        public void Init(HttpApplication context)
        {
            context.BeginRequest += new EventHandler(OnBeginRequest);
            application = context;
        }

        #endregion
        void OnBeginRequest(object sender, EventArgs e)
        {
            if (application.Request.AppRelativeCurrentExecutionFilePath.Contains(".aspx"))
            {
                application.Response.Filter = new AbsolutePathRewriterStream(application.Response.Filter);
            }
        }
    }

    public class AbsolutePathRewriterStream : MemoryStream
    {
        private Stream outputStream = null;

        public AbsolutePathRewriterStream(Stream output)
        {
            outputStream = output;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            string bufferContent = UTF8Encoding.UTF8.GetString(buffer);
            string absolutePath = HttpContext.Current.Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Authority;
            bufferContent = bufferContent.Replace("href=\"/", "href=\"" + absolutePath + "/");
            bufferContent = bufferContent.Replace("href='/", "href='/" + absolutePath + "/");
            outputStream.Write(UTF8Encoding.UTF8.GetBytes(bufferContent), offset, UTF8Encoding.UTF8.GetByteCount(bufferContent));
            base.Write(buffer, offset, count);
        }
    }
}


MVC Routes and SEO


I’ve been looking at Micrsofts MVC engine lately and I can’t help but think why isn’t the default route more SEO friendly.

When I create a new project this code is placed inside of my global.asax.cs file.

public static void RegisterRoutes(RouteCollection routes)
 {
 routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

 routes.MapRoute(
 "Default", // Route name
 "{controller}/{action}/{id}", // URL with parameters
 new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
 );

 }

That will give me a product url like this :

www.somewebsite.com/products/view/1.aspx

But I’d rather have a friendly name in there somewhere like this :

www.somewebsite.com/products/view/my-friendly-product-name/1.aspx

So I think if you go with defualt routing you’re making a mistake.