Frederik Vig – ASP.NET developer

Follow me

Archive for the ‘Code Snippet’ Category.

Adding different CSS classes when using the EPiServer PageTree control

Another little quick tip. I was browsing the EPiServer World forum and came across a common question.

by David Green

I am using the EpiServer:PageTree control to generate a nested <ul><li> list in the format below.

However I am also using a dropdown menu system called UDM which requires that the class and id “udm” are on the first <ul> tag.

The container object of the PageTree controller exposes the Indent property that we can use for this. Below is a simple example of how to add a CSS class to the first ul in your list (code is based on SubMenu.ascx from the EPiServer Public Templates package).

<%@ Control Language="C#" EnableViewState="false" AutoEventWireup="False" CodeBehind="SubMenu.ascx.cs" Inherits="EPiServer.Templates.Public.Units.Static.SubMenu" %>
<EPiServer:PageTree ShowRootPage="false" runat="server" id="Menu">
    <IndentTemplate>
        <ul <%# AddCssClassToFirstLevel(Container.Indent, "udm") %>>
    </IndentTemplate>
    <ItemHeaderTemplate>
        <li>
    </ItemHeaderTemplate>
    <ItemTemplate>
        <EPiServer:Property PropertyName="PageLink" runat="server" />
    </ItemTemplate>
    <SelectedItemTemplate>
	<EPiServer:Property CssClass="selected" PropertyName="PageName" runat="server" />
    </SelectedItemTemplate>
    <ItemFooterTemplate>
        </li>
    </ItemFooterTemplate>
    <UnindentTemplate>
        </ul>
    </UnindentTemplate>
</EPiServer:PageTree>
using System;
using EPiServer;
using EPiServer.Web.WebControls;
 
namespace EPiServer.Templates.Public.Units.Static
{
    public partial class SubMenu : UserControlBase
    {
        private MenuList _menuList;
 
        /// <summary>
        /// Gets or sets the data source for this control
        /// </summary>
        public MenuList MenuList
        {
            get { return _menuList; }
            set { _menuList = value; }
        }
 
        protected override void OnLoad(System.EventArgs e)
        {
            base.OnLoad(e);
 
            if (MenuList == null)
            {
                return;
            }
            Menu.PageLink = MenuList.OpenTopPage;
            Menu.DataBind();
        }
 
        protected string AddCssClassToFirstLevel(int level, string cssClassName)
        {
            if (level == 1)
            {
                return string.Format("class=\"{0}\"", cssClassName);
            }
 
            return string.Empty;
        }
    }
}

We can easily add more complex logic. We also have access to the HasChildren property, which tells us if the active page has any children.

Hope this helps.

Sending confirmation email to the user when using EPiServer XForms

Plenty of websites have contact forms or other forms that users fills out to either make a request or provide feedback. This is something that many sites use instead of making their email address public available. To avoid receiving to much spam. The thing that I don’t like about these forms is that mostly I don’t get any confirmation after I’ve filled it out.

This is also true when using EPiServer XForms. When creating a new form the editor can choose between saving the form data in the database, emailing it, or doing both. Sending a confirmation email to the user is not a choice.

Example

If we take a look at the documentation – Developing with XForms – we see that we have the AfterSubmitPostedData event that we can use to add the extra logic we need :) .

Okay lets open up our EPiServer project in Visual Studio and go to the Global.asax.cs file. Here we see the different methods being attached to various XForm events.

public void XForm_ControlSetup(object sender, EventArgs e)
{
    XFormControl control = (XFormControl)sender;
 
    control.BeforeLoadingForm += new LoadFormEventHandler(XForm_BeforeLoadingForm);
    control.ControlsCreated += new EventHandler(XForm_ControlsCreated);
    control.BeforeSubmitPostedData += new SaveFormDataEventHandler(XForm_BeforeSubmitPostedData);
    control.AfterSubmitPostedData += new SaveFormDataEventHandler(XForm_AfterSubmitPostedData);
}

Lets go down to the XForm_AfterSubmitPostedData method. Before we can start coding we need to figure out a way to get the users email address. If this is a community site where users register and provide this information we can just get it from there. But if not we need them to provide it for us. The easiest way is to use a TextBox where the user can type in their email address. To be able to recognize this TextBox in our code we need to use the same name in every form that needs this functionality.

In the code below I’ve hard coded the TextBox name (UserEmail), but you can make it more flexible by storing it in a Property or the web.config file.

public void XForm_AfterSubmitPostedData(object sender, SaveFormDataEventArgs e)
{
	var control = (XFormControl)sender;
	var pageBase = control.Page as PageBase;
 
	string emailaddress = e.FormData.GetValue("UserEmail");
	if (!string.IsNullOrEmpty(emailaddress))
	{
		e.FormData.MailTo = emailaddress;
		EmailHelper.SendUserEmail(e.FormData, pageBase.CurrentPage);
	}
 
	if (control.FormDefinition.PageAfterPost != 0)
	{
		PageData redirectPage = DataFactory.Instance.GetPage(new PageReference(control.FormDefinition.PageAfterPost));
		control.Page.Response.Redirect(redirectPage.LinkURL);
		return;
	}
 
	//After the form has been posted we remove the form elements and add a "thank you message".
	control.Controls.Clear();
	Label label = new Label();
	label.CssClass = "thankyoumessage";
	label.Text = LanguageManager.Instance.Translate("/form/postedmessage");
	control.Controls.Add(label);
}

The EmailHelper class with the SendUserEmail method.

using System;
using System.Collections.Specialized;
using System.Net.Mail;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using EPiServer.Core;
using EPiServer.XForms;
 
namespace FV.Templates.FV.Util
{
    public static class EmailHelper
    {
        public static bool SendUserEmail(XFormData formData, PageData currentPage)
        {
            if (formData == null)
            {
                return false;
            }
 
            try
            {
                var message = new MailMessage();
                var mailFrom = string.Format("no-reply@{0}", HttpContext.Current.Request.Url.Host);
 
                if (!string.IsNullOrEmpty(formData.MailFrom))
                {
                    mailFrom = formData.MailFrom;
                }
 
                message.From = new MailAddress(mailFrom);
                message.To.Add(formData.MailTo);
 
                message.Subject = formData.MailSubject;
                message.IsBodyHtml = true;
                message.BodyEncoding = Encoding.UTF8;
 
                var body = new StringBuilder();
                body.Append("<html><body><table border=\"0\">");
 
                NameValueCollection postedValues = formData.GetValues();
 
                foreach (string key in postedValues)
                {
                    body.Append("<tr><td>");
                    body.Append(key).Append(": ");
                    body.Append("</td><td>");
                    body.Append(postedValues[key]);
                    body.Append("</td></tr>");
                }
 
                body.Append("</table></body></html>");
 
                message.Body = body.ToString();
                var smtp = new SmtpClient();
                smtp.Send(message);
                return true;
            }
            catch (Exception ex)
            {
                // Remember to do some logging here..
                return false;
            }
        }
    }
}

Since we both have the XFormData object and the CurrentPage PageData object we can do a lot more than what I’ve illustrated in the code above. You can for instance create an email template in EPiServer that editors can edit that you use to send to as a standard “thank you email” to the user.

Testing

To test this code create a new XForm and add a TextBox with the name UserEmail.

Try sending it, you should receive an email with the form data (form fields and their values). Remember that for this to work you also need to configure the SMTP settings in your web.config file.

When developing locally, I usually set it to point to a local directory that the emails get sent to.

<mailSettings>
	<smtp deliveryMethod="SpecifiedPickupDirectory">
                <!-- You need to give the Network Service or ASPNET user modify permissions on your directory  -->
		<specifiedPickupDirectory pickupDirectoryLocation="E:\temp\maildrop"/>
	</smtp>
</mailSettings>

You should now receive an email that looks something like this.

Download the code

Other resources

Creating a Custom EPiServer Paging Control

Earlier I wrote about the PageList control from EPiServer, I mentioned in that post that the markup that the PagingControl renders is not the best. For instance it will not work without JavaScript enabled on the client (for search engines the links wont work). I therefor decided to create my own.

To start with I went to the EPiServer SDK to have a look at the PagingControl class. Here I was able to see the namespace and what .dll file it lives in (EPiServer.dll). I then opened up Reflector, to inspect EPiServer.dll and have a closer look at the code.

Description of Reflector by Red Gate.

.NET Reflector enables you to easily view, navigate, and search through, the class hierarchies of .NET assemblies, even if you don’t have the code for them. With it, you can decompile and analyze .NET assemblies in C#, Visual Basic, and IL.

To find the PagingControl class you can either use the search feature or navigate the namespaces.

PagingControl class with Reflector

The great thing is that all the methods are virtual which means that we can override them :) .

Start by creating a new class and inherit from PagingControl (remember to add using EPiServer.Web.WebControls;).

using System;
using System.Text;
using System.Web;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using EPiServer;
using EPiServer.Core;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Classes
{
    public class CustomPager : PagingControl
    {
    }
}

Then create a new page template with a PageList control.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Page.aspx.cs" Inherits="FV.Templates.FV.Pages.Page" MasterPageFile="~/Templates/Public/MasterPages/MasterPage.master" %>
<asp:Content runat="server" ContentPlaceHolderID="MainBodyRegion">
    <EPiServer:PageList runat="server" ID="pglList">
        <HeaderTemplate>
            <ul>
        </HeaderTemplate>
        <ItemTemplate>
            <li><EPiServer:Property runat="server" PropertyName="PageLink" /></li>
        </ItemTemplate>
        <FooterTemplate>
            </ul>
        </FooterTemplate>
    </EPiServer:PageList>
</asp:Content>
using System;
using EPiServer;
using EPiServer.Core;
using FV.Templates.FV.Classes;
 
namespace FV.Templates.FV.Pages
{
    public partial class Page : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
 
            pglList.Paging = true;
            pglList.PageLink = PageReference.StartPage;
            pglList.PagingControl = new CustomPager();
            pglList.PagesPerPagingItem = 2;
        }
    }
}

Notice that we set the PageList’s PagingControl property to a new instance of our CustomPager web control.

If you open up a browser and run the code, you’ll just get a standard PagingControl with the JavaScript and bad markup (since we haven’t overridden anything yet).

Lets start by adding an ordered list for the paging items.

To do this we have to override the CreatePagingItems, AddSelectedPagingLink and AddUnselectedPagingLink methods.

using System;
using System.Text;
using System.Web;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using EPiServer;
using EPiServer.Core;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Classes
{
    public class CustomPager : PagingControl
    {
        private HtmlGenericControl Container
        {
            get; set;
        }
 
        private static string Translate(string text)
        {
            return LanguageManager.Instance.Translate("/webcontrols/paging/" + text);
        }
 
        protected override LinkButton AddSelectedPagingLink(int pagingIndex, string text, string altText)
        {
            var li = new HtmlGenericControl("li");
            li.Attributes.Add("class", this.CssClassSelected);
            li.Attributes.Add("title", altText);
            li.InnerText = text;
            this.Container.Controls.Add(li);
            return null;
        }
 
        protected override LinkButton AddUnselectedPagingLink(int pagingIndex, string text, string altText, bool visible)
        {
            var li = new HtmlGenericControl("li");
            var child = this.CreatePagingLink(pagingIndex, text, altText);
            child.CssClass = this.CssClassUnselected;
            li.Visible = visible;
 
            li.Controls.Add(child);
            this.Container.Controls.Add(li);
            return null;
        }
 
        public override void CreatePagingItems(PageDataCollection pages)
        {
            int pagingIndex = this.CurrentPagingItemCount - 1;
            bool visible = this.CurrentPagingItemIndex > 0;
            bool flag2 = this.CurrentPagingItemIndex < pagingIndex;
            if ((pagingIndex != 0) || !this.AutoPaging)
            {
                this.Container = new HtmlGenericControl("ol");
                this.LinkCounter = 0;
                if (this.FirstPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(0, this.FirstPagingItemText, Translate("firstpage"), visible);
                }
                if (this.PrevPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex - 1, this.PrevPagingItemText, Translate("prevpage"), visible);
                }
                for (int i = 0; i <= pagingIndex; i++)
                {
                    if (i == this.CurrentPagingItemIndex)
                    {
                        this.AddSelectedPagingLink(i, Convert.ToString((int)(i + 1)), Translate("currentpage"));
                    }
                    else
                    {
                        this.AddUnselectedPagingLink(i, Convert.ToString((int)(i + 1)), string.Format(Translate("pagenumber"), i + 1), true);
                    }
                }
                if (this.NextPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex + 1, this.NextPagingItemText, Translate("nextpage"), flag2);
                }
                if (this.LastPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(pagingIndex, this.LastPagingItemText, Translate("lastpage"), flag2);
                }
 
                this.Controls.Add(this.Container);
            }
        }
    }
}

I added a private property to hold my ordered list (Container), so that I can easily add child controls to it. I Also removed the calls to the AddLinkSpacing method (since we don’t need it).

If you browse the page you’ll see that everything is inside an ordered list now.

To fix the JavaScript links I created a new method CreatePagingHyperLink that will create the HyperLink control with the correct url. I Then added code in the OnInit method to get the query string and set the PageList’s CurrentPagingItemIndex property.

using System;
using System.Text;
using System.Web;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using EPiServer;
using EPiServer.Core;
using EPiServer.Web;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Classes
{
    public class CustomPager : PagingControl
    {
        private HtmlGenericControl Container
        {
            get; set;
        }
 
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
 
            if (HttpContext.Current.Request.QueryString["p"] == null)
            {
                return;
            }
 
            var p = HttpUtility.HtmlEncode(HttpContext.Current.Request.QueryString["p"]);
            int num;
 
            int.TryParse(p, out num);
            this.CurrentPagingItemIndex = num;
        }
 
        private static string Translate(string text)
        {
            return LanguageManager.Instance.Translate("/webcontrols/paging/" + text);
        }
 
        protected override LinkButton AddSelectedPagingLink(int pagingIndex, string text, string altText)
        {
            var li = new HtmlGenericControl("li");
            li.Attributes.Add("class", this.CssClassSelected);
            li.Attributes.Add("title", altText);
            li.InnerText = text;
            this.Container.Controls.Add(li);
            return null;
        }
 
        protected override LinkButton AddUnselectedPagingLink(int pagingIndex, string text, string altText, bool visible)
        {
            var li = new HtmlGenericControl("li");
            var child = this.CreatePagingHyperLink(pagingIndex, text, altText);
            child.CssClass = this.CssClassUnselected;
            li.Visible = visible;
 
            li.Controls.Add(child);
            this.Container.Controls.Add(li);
            return null;
        }
 
        private static string CreateUrl(int count)
        {
            var url = new UrlBuilder(HttpContext.Current.Request.Url);
 
            if (UrlRewriteProvider.IsFurlEnabled)
            {
                Global.UrlRewriteProvider.ConvertToExternal(url, null, Encoding.UTF8);
            }
 
            return UriSupport.AddQueryString(url.ToString(), "p", count.ToString());
        }
 
        protected HyperLink CreatePagingHyperLink(int pagingIndex, string text, string altText)
        {
            var link = new HyperLink();
            this.LinkCounter++;
            link.ID = "PagingID" + this.LinkCounter;
            link.NavigateUrl = CreateUrl(pagingIndex);
            link.Text = text;
            link.ToolTip = altText;
 
            return link;
        }
 
        public override void CreatePagingItems(PageDataCollection pages)
        {
            int pagingIndex = this.CurrentPagingItemCount - 1;
            bool visible = this.CurrentPagingItemIndex > 0;
            bool flag2 = this.CurrentPagingItemIndex < pagingIndex;
            if ((pagingIndex != 0) || !this.AutoPaging)
            {
                this.Container = new HtmlGenericControl("ol");
                this.LinkCounter = 0;
                if (this.FirstPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(0, this.FirstPagingItemText, Translate("firstpage"), visible);
                }
                if (this.PrevPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex - 1, this.PrevPagingItemText, Translate("prevpage"), visible);
                }
                for (int i = 0; i <= pagingIndex; i++)
                {
                    if (i == this.CurrentPagingItemIndex)
                    {
                        this.AddSelectedPagingLink(i, Convert.ToString((int)(i + 1)), Translate("currentpage"));
                    }
                    else
                    {
                        this.AddUnselectedPagingLink(i, Convert.ToString((int)(i + 1)), string.Format(Translate("pagenumber"), i + 1), true);
                    }
                }
                if (this.NextPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex + 1, this.NextPagingItemText, Translate("nextpage"), flag2);
                }
                if (this.LastPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(pagingIndex, this.LastPagingItemText, Translate("lastpage"), flag2);
                }
 
                this.Controls.Add(this.Container);
            }
        }
    }
}

I also added some CSS code to give you an idea of how easy it is to style and change the look.

.PagingContainer ol {
    margin: 0;
    padding: 0;
    overflow: hidden;
}
 
.PagingContainer li {
    list-style: none;
    display: inline;
}
 
.PagingContainer a, .SelectedPagingItem {
    text-decoration: none;
    float: left;
    padding: .2em;
    border: 1px solid #303233;
    color: #303233;
    font-weight: bold;
    margin-right: .1em;
}
 
.PagingContainer .SelectedPagingItem {
    background: #303233;
    color: #fff;
}

The result

Custom paging control

Markup rendered

<div class="PagingContainer">
        <ol>
            <li class="SelectedPagingItem" title="Current page">1</li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID4" title="Page 2" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=1">2</a>
            </li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID5" title="Page 3" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=2">3</a>
            </li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID6" title="Next page" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=1">></a>
            </li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID7" title="Last page" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=2">»</a>
            </li>
        </ol>
    </div>

Download the code

Extending search field with suggestion box

Disclaimer: In this example I’ve kept the code simple to make it easier to read and to give you an idea of how you might approach something like this with EPiServer. This code should not be used in production scenarios since it will use a lot of resources. Use at own risk :) .

Update – 12.10.2009

Updated production code with a new custom filter that removes pages that the Everyone role doesn’t have access to.
Also updated the Get caching method. See comments below for further details.

Update – 19.09.2009

Added example of production code you might consider.

We’ve all used Google Suggest before. When you start typing a word or sentence, Google comes up with suggestions to what we might be searching for.
Example of Google Suggest
This is to help users quickly find what they want.

In this blog post we’re going to do the same by extending QuickSearch that ships with the EPiServer Public Templates.

I decided to use jQuery and the AutoComplete plugin for this, but you can use any script you like (most are pretty similar).

The result

Example of search suggest

The Code

First start by modifying the QuickSearch.ascx:

<asp:Panel runat="server" CssClass="QuickSearchArea" DefaultButton="SearchButton">
	<asp:Label ID="SearchLabel" runat="server" AssociatedControlID="SearchText" CssClass="hidden" Text="<%$ Resources: EPiServer, navigation.search %>" />
    <asp:TextBox ID="SearchText" TabIndex="1" runat="server" CssClass="quickSearchField" autocomplete="off" />
    <asp:ImageButton ID="SearchButton" runat="server" ImageUrl="~/Templates/Public/Images/MainMenuSearchButton.png" ToolTip="<%$ Resources: EPiServer, navigation.search %>" CausesValidation="false" CssClass="quickSearchButton" OnClick="Search_Click" />
</asp:Panel>
 
<script type="text/javascript">
//<![CDATA[ 
    $("#<%=SearchText.ClientID %>").autocomplete("/path/to/file/SearchSuggest.aspx", {
        minChars: 2,
        max: 10
    });
//]]> 
</script>

A few things to note. I removed the default text from the TextBox and added the attribute autocomplete=”off”, this is to ensure that the users browser does not store the information (How to Turn Off Form Autocompletion). I then attached the AutoComplete plugin to the TextBox and changed a few of its default properties.

As you can see there is not much code that is needed when mostly using the default settings. You can of course do a lot more. Browse through the AutoComplete plugin documentation and see the demos to get a few ideas.

Now for the main part, SearchSuggestion.aspx. This is just an empty web forms page with all the code in the code-behind:

using System;
using System.Text;
using System.Web;
using EPiServer;
using EPiServer.Core;
using EPiServer.Filters;
using EPiServer.Security;
 
namespace FV.Templates.FV.Pages
{
    public partial class SearchSuggest : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            // #1
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
 
                // #2
                var criterias = new PropertyCriteriaCollection();
 
                var queryCriteria = new PropertyCriteria
                                        {
                                            Condition = CompareCondition.StartsWith,
                                            Name = "PageName",
                                            Value = query,
                                            Type = PropertyDataType.String,
                                            Required = true
                                        };
 
                var pubCriteria = new PropertyCriteria
                                      {
                                          Condition = CompareCondition.Equal,
                                          Name = "PagePendingPublish",
                                          Value = false.ToString(),
                                          Type = PropertyDataType.Boolean,
                                          Required = true
                                      };
 
                criterias.Add(queryCriteria);
                criterias.Add(pubCriteria);
 
                // #3
                var pages = DataFactory.Instance.FindPagesWithCriteria(PageReference.StartPage, criterias, CurrentPage.LanguageBranch);
 
                int tempLimit = Convert.ToInt32(limit);
                int pagesCount = pages.Count;
 
                // #4
                if (tempLimit > pagesCount)
                {
                    tempLimit = pagesCount;
                }
 
                // #5
                for (int i = 0; i < tempLimit; i++)
                {
                    sb.Append(pages[i].PageName);
                    sb.Append(Environment.NewLine);
                }
 
                // #6
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
    }
}
  • In #1 we retrieve the user input and the limit (which we set earlier with the max property in QuickSearch.ascx), we then HTML encode it (something you should always do when receiving user input).
  • In #2 we build the criteria collection with our search options. We search for pages that start with what the user typed in and then make sure that only published pages are returned.
  • In #3 we do the actual searching with the FindPagesWithCriteria method where we also set the current language branch (only return pages in that language).
  • In #4 we make sure that we don’t have less pages than the limit, if so we set the pages count to be the limit.
  • In #5 we go through pages (PageDataCollection) and add the PageName to sb (StringBuilder).
  • In #6 we return the result back

Below is the code for SearchSuggest.aspx.cs updated to use the SearchDataSource instead of FindPagesWithCriteria (this will not only search the PageName but all properties that are search able):

using System;
using System.Text;
using System.Web;
using System.Web.UI;
using EPiServer;
using EPiServer.Core;
using EPiServer.Security;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Pages
{
    public partial class SearchSuggest : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
 
                var searchDataSource = new SearchDataSource
                {
                    SearchQuery = query,
                    AccessLevel = AccessLevel.Read,
                    PublishedStatus = PagePublishedStatus.Published,
                    PageLink = PageReference.StartPage,
                    LanguageBranches = CurrentPage.LanguageBranch,
                    MaxCount = Convert.ToInt32(limit)
                };
 
                foreach (PageData page in searchDataSource.Select(DataSourceSelectArguments.Empty))
                {
                    sb.Append(page.PageName);
                    sb.Append(Environment.NewLine);
                }
 
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
    }
}

Production code

Like I said in the beginning, this code should not be used in production scenarios since it goes through the entire site searching for matching PageNames each time there is a request.

I’ve added some example code of how you might approach this in a production scenario. Basically I just use the FindPagesWithCriteria method to find all the pages and then store the PageName of each in an array that I then store in the cache for easy access.

using System;
using System.Linq;
using System.Text;
using System.Web;
using EPiServer;
using EPiServer.Core;
using EPiServer.Filters;
using EPiServer.Security;
using FV.Templates.FV.Filters;
 
namespace FV.Templates.FV.Pages
{
    public partial class ProductionSearchSuggestion : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
                string[] pageNames;
 
                const string pageNamesCacheKey = "AllPageNames";
 
                if (!Get(pageNamesCacheKey, out pageNames))
                {
                    var criterias = new PropertyCriteriaCollection();
 
                    var pubCriteria = new PropertyCriteria
                    {
                        Condition = CompareCondition.Equal,
                        Name = "PagePendingPublish",
                        Value = false.ToString(),
                        Type = PropertyDataType.Boolean,
                        Required = true
                    };
 
                    criterias.Add(pubCriteria);
 
                    var pages = DataFactory.Instance.FindPagesWithCriteria(PageReference.StartPage, criterias, CurrentPage.LanguageBranch);
 
                    new FilterRole("Everyone").Filter(pages);
 
                    var pagesCount = pages.Count;
                    pageNames = new string[pagesCount];
 
                    for (int i = 0; i < pagesCount; i++)
                    {
                        pageNames[i] = pages[i].PageName;
                    }
 
                    Add(pageNames, pageNamesCacheKey);
                }
 
                int tempLimit = Convert.ToInt32(limit);
 
                var result = new string[tempLimit];
                int resultCounter = 0;
 
                for (int i = 0; i < pageNames.Length; i++)
                {
                    if (resultCounter <= tempLimit && pageNames[i].ToLower().StartsWith(query.ToLower()) && !result.Contains(pageNames[i]))
                    {
                        result[resultCounter++] = pageNames[i];
                    }
                }
 
                for (int i = 0; i < resultCounter; i++)
                {
                    sb.Append(result[i]);
                    sb.Append(Environment.NewLine);
                }
 
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
 
        // Caching methods from http://johnnycoder.com/blog/2008/12/10/c-cache-helper-class/
        public bool Exists(string key)
        {
            return HttpContext.Current.Cache[key] != null;
        }
 
        public bool Get<T>(string key, out T value) where T:class
        {
            try
            {
                value = HttpContext.Current.Cache[key] as T;
                if (value == null)
                {
                    value = default(T);
                    return false;
                }
            }
            catch
            {
                value = default(T);
                return false;
            }
 
            return true;
        }
 
        public void Add<T>(T o, string key)
        {
            HttpContext.Current.Cache.Insert(
                key,
                o,
                null,
                DateTime.Now.AddMinutes(3600),
                System.Web.Caching.Cache.NoSlidingExpiration);
        }
    }
}

The FilterRole class

using System;
using EPiServer.Core;
using EPiServer.Filters;
 
namespace FV.Templates.FV.Filters
{
    public class FilterRole : IPageFilter
    {
        public string RoleName
        {
            get;
            set;
        }
 
        public FilterRole()
        {
        }
 
        public FilterRole(string roleName)
        {
            this.RoleName = roleName;
        }
 
        public void Filter(PageDataCollection pages)
        {
            for (int i = pages.Count - 1; i >= 0; i--)
            {
                bool isMatch = false;
                var acl = pages[i].ACL;
 
                if (acl != null)
                {
                    foreach (var role in acl)
                    {
                        if (role.Key == this.RoleName)
                        {
                            isMatch = true;
                        }
                    }
                }
 
                if (!isMatch)
                {
                    pages.RemoveAt(i);
                }
            }
        }
 
        public void Filter(object sender, FilterEventArgs e)
        {
            this.Filter(e.Pages);
        }
 
        public bool ShouldFilter(PageData page)
        {
            throw new NotImplementedException();
        }
    }
}

Download the code

Add class attribute to body element with ASP.NET web forms and Master Pages

Often I have to add a class to the HTML body element. This is a little code snippet that allows you to easily do this.

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site1.master.cs" Inherits="WebApplication1.Site1" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body runat="server" id="BodyTag">
    <form id="form1" runat="server">
    ...
    </form>
</body>
</html>

Add the public property BodyClass.

public string BodyClass
{
    set
    {
        BodyTag.Attributes.Add("class", value);
    }
}

Then in your .aspx, you need to make the Master Page strongly typed by adding MasterType (you can also manually cast it to the right type).

<%@ MasterType VirtualPath="~/Site1.Master" %>

You’ll now have access to the Master Pages BodyClass property.

Master.BodyClass = "home";
 
// Or by manually casting it
((Site1)Master).BodyClass = "home";
...
<body id="ctl00_BodyTag" class="home">
...

EPiServer filter – part 2: create your own filter

If you haven’t, be sure to check out EPiServer filter – part 1.
In one of my project I had a bunch of categories and needed to filter a PageDataCollection to find pages that had one or more of the categories. Out of the box EPiServer has no filter class that does this, so I needed to create my own. This was actually pretty easy. I created a new class and implemented the EPiServer.Filters.IPageFilter interface which has three method signatures.

void Filter(EPiServer.Core.PageDataCollection pages);
void Filter(object sender, EPiServer.Filters.FilterEventArgs e);
bool ShouldFilter(EPiServer.Core.PageData page);

I then added a public property of type CategoryList for my categories. Then in my filter method I traversed the PageDataCollection and looked for matches, if there was no match the page got removed.

The complete code

public class FilterAtLeastOneCategory : IPageFilter
{
	public CategoryList Categories
	{
	    get;
	    set;
	}
 
	public FilterAtLeastOneCategory()
	{
	}
 
	public FilterAtLeastOneCategory(CategoryList categoryList)
	{
	    this.Categories = categoryList;
	}
 
	public void Filter(PageDataCollection pages)
	{
	    for (int i = pages.Count - 1; i >= 0; i--)
	    {
		bool isMatch = false;
		var categoryList = pages[i].Category;
 
		if (categoryList != null)
		{
		    foreach (var category in categoryList)
		    {
			foreach (var currentCategory in this.Categories)
			{
			    if (category == currentCategory)
			    {
				isMatch = true;
			    }
			}
		    }
		}
 
		if (!isMatch)
		{
		    pages.RemoveAt(i);
		}
	    }
	}
 
	public void Filter(object sender, FilterEventArgs e)
	{
	    this.Filter(e.Pages);
	}
 
	public bool ShouldFilter(PageData page)
	{
	    throw new NotImplementedException();
	}
}

I could then use my new filter class.

var currentCategories = CurrentPage.Category;
new FilterAtLeastOneCategory(currentCategories).Filter(myPageDataCollection);

Filter by Role

Here is another custom filter that removes all pages that the specified role doesn’t have access to.

using System;
using EPiServer.Core;
using EPiServer.Filters;
 
namespace FV.Templates.FV.Filters
{
    public class FilterRole : IPageFilter
    {
        public string RoleName
        {
            get;
            set;
        }
 
        public FilterRole()
        {
        }
 
        public FilterRole(string roleName)
        {
            this.RoleName = roleName;
        }
 
        public void Filter(PageDataCollection pages)
        {
            for (int i = pages.Count - 1; i >= 0; i--)
            {
                bool isMatch = false;
                var acl = pages[i].ACL;
 
                if (acl != null)
                {
                    foreach (var role in acl)
                    {
                        if (role.Key == this.RoleName)
                        {
                            isMatch = true;
                        }
                    }
                }
 
                if (!isMatch)
                {
                    pages.RemoveAt(i);
                }
            }
        }
 
        public void Filter(object sender, FilterEventArgs e)
        {
            this.Filter(e.Pages);
        }
 
        public bool ShouldFilter(PageData page)
        {
            throw new NotImplementedException();
        }
    }
}
new FilterRole("Everyone").Filter(pages);

CSS tricks – part 1

Here are a few CSS tricks I’ve picked up along the way.

Always visible vertical scrollbar

When you have a fixed width centered web site open you’ve probably noticed that the page sometimes jumps a little. This is because the vertical scrollbar only is visible if the content is longer than the viewport, if no vertical scrollbar is visible in the browser. To always have the vertical scrollbar visible you can use the CSS hack below.

html {
    overflow-y: scroll;	
}

CSS reset styles

To make our lives as web developers easier we have a few css reset style rules which will help make things more consistent across browsers. It started out with simply setting the padding and margin of all elements to 0 with this CSS code.

* {
    margin: 0;
    padding: 0;
}

Though this caused a few problems with form elements. After a few years of using the technique above I started using a CSS reset stylesheet.

100% min-height

#wrapper {
    min-height: 600px;
    background-color: #000;
}
* html #wrapper {
    height: 600px;
}

Internet Explorer 6 has problems understanding the min-height property. Therefor I use the star selector hack to target IE6 and set the height property (IE6 uses the height property the same way as min-height, so if the content is bigger the page will still grow, like with min-height).

More information about the min-height property

Star selector hack

Applying * html {selector here} will only target Internet Explorer 5.5 and 6. Which makes this a nice way to still use valid CSS and only target those two browsers. Like in the min-height example above.

More information: Workarounds and Filters

Internet Explorer 6 double margin bug

When setting the float property to either left or right and having a margin set in the same direction as the float (either margin-left or margin-right) will give IE6 users twice the margin.

#sidebar {
    float: left;
    margin-left: 10px;
}

This will give IE6 users a left margin of 20px. Fortunately it is easy to fix. Simply add the display property and set it to inline.

#sidebar {  
    float: left;
    margin-left: 10px;
    display: inline;
}

This has no negative effect on other browsers. Also, when floating inline elements (a, span, em, etc), the element automatically becomes the same as when using display:block.

More on floats

hasLayout in Internet Explorer

“Layout” is an IE/Win proprietary concept that determines how elements draw and bound their content, interact with and relate to other elements, and react on and transmit application/user events.

This quality can be irreversibly triggered by some CSS properties. Some HTML elements have “layout” by default.

Microsoft developers decided that elements should be able to acquire a “property” (in an object-oriented programming sense) they referred to as hasLayout, which is set to true when this rendering concept takes effect.

Source: satzansatz.de

I mostly come across this when I have no width or height applied. Like in the code below.

#wrapper {
    overflow: hidden;
}
#sidebar {
    float: left;
    width: 200px;
}
#content {
    float: left;
    width: 600px;
}

Overflow: hidden will clear the floats in all browsers except for in IE6. I could fix this by giving #wrapper a width or by floating it. But that is not always possible. Instead I can use the star selector and give IE6 a height of 1% to just trigger hasLayout.

#wrapper {
    overflow: hidden;
}
* html #wrapper {
    height: 1%;
}
#sidebar {
    float: left;
    width: 200px;
}
#content {
    float: left;
    width: 600px;
}

More information on hasLayout

Displaying a pointer cursor for button, label and select elements

I always add this code when I start a new project.

input[type=submit], button, label, select { 
    cursor: pointer; 
}

Just to make it easier for users to see where they can click.

EPiServer filter – part 1

Often we need to filter some of the pages in our PageDataCollection. Perhaps we wish to only show the pages that are published or that the user has access to. For this EPiServer has a few filter classes. Below you’ll find some examples. More information in the EPiServer SDK under the EPiServer.Filter namespace

To start we create a PageDataCollection of the current pages children

var news = GetChildren(CurrentPage.PageLink);

Remember to add using EPiServer.Filters; to use the filter classes.

Remove pages that are not published

EPiServer 4

new FilterPublished().Filter(this, new FilterEventArgs(news));

EPiServer 5

new FilterPublished(PagePublishedStatus.Published).Filter(news);

More information about the PagePublishedStatus enum in the SDK.

Filter by both published and read access

EPiServer 5

FilterForVisitor.Filter(news);

Filter by access level

If the user does not have at least Edit access the page will be removed.

EpiServer 5

// using EPiServer.Security;
new FilterAccess(AccessLevel.Edit).Filter(news);

More information about the AccessLevel enum in the SDK.

Sort the collection after a property value

EPiServer 5

// Default is Ascending
new FilterPropertySort("PageName").Filter(news);
new FilterPropertySort("PageName", FilterSortDirection.Descending).Filter(news);

Filter out pages where a property has no value

EPiServer 5

new FilterRemoveNullValues("MainBody").Filter(news);

Filter by a custom property

EPiServer 5

new FilterCompareTo("PageVisibleInMenu", "true").Filter(news);

In the EPiServer SDK there is a nice list of EPiServer properties.

Master Pages and EPiServer CurrentPage property

When you use user controls or web forms in your EPiServer web application you usually inherit from either UserControlBase or TemplatePage. Which gives you, among other things, the CurrentPage property. The problem is when you need the CurrentPage property in your Master Page.

The way I usually fix this is with this code.

var page = (PageBase) Page;
...
page.CurrentPage.PageName;
page.CurrentPage.LinkURL;
...

This should work fine in both EPiServer 4 and 5.

EPiServer Link Collection Property

In EPiServer CMS 5 R2, EPiServer added a new property called the link collection. This property allows you to add links to web pages, documents and e-mail addresses. For a nice overview see this post: EPiServer 5 R2 and Link Collection property

In this example I’m going to show you how to use the link collection property to get a page reference to EPiServer pages and retrieve some content from them. This could be used for a related content sidebar or something similar.

Start by creating a new link collection property in admin mode to an existing page type or a new one, name it RelatedContent. Then go to edit mode and add a few links to various pages.

Now we’ll start on the page template, open up your web form and add this markup.

<asp:Repeater runat="server" ID="rptRelatedContent" OnItemDataBound="rptRelatedContent_ItemDataBound">
    <HeaderTemplate>
	<ul id="relatedContent">
    </HeaderTemplate>
    <ItemTemplate>
	<li>
	    <h3><asp:HyperLink runat="server" ID="lnkHeading" /></h3>
	    <p><asp:Literal runat="server" ID="ltlText" /></p>
	</li>
    </ItemTemplate>
    <FooterTemplate>
	</ul>
    </FooterTemplate>
</asp:Repeater>

Then in your code-behind.

protected void Page_Load(object sender, EventArgs e)
{
    if (IsValue("RelatedContent"))
    {
	// using EPiServer.SpecializedProperties;
	var relatedContent = (LinkItemCollection)CurrentPage["RelatedContent"];
 
	if (relatedContent != null)
	{
	    rptRelatedContent.DataSource = relatedContent;
	    rptRelatedContent.DataBind();
	}
    }
}
protected void rptRelatedContent_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
    {
	var linkItem = (LinkItem)e.Item.DataItem;
	var lnkHeading = (HyperLink)e.Item.FindControl("lnkHeading");
	var ltlText = (Literal)e.Item.FindControl("ltlText");
 
	if (linkItem != null && lnkHeading != null && ltlText != null)
	{
	    var url = new UrlBuilder(linkItem.Href);
 
	    // using EPiServer.Web;
	    bool isEPiServerPage = PermanentLinkMapStore.ToMapped(url);
 
	    if (isEPiServerPage)
	    {
		var page = DataFactory.Instance.GetPage(PermanentLinkUtility.GetPageReference(url));
 
		if (page != null)
		{
		    lnkHeading.Text = page["Heading"] as string ?? page.PageName;
		    lnkHeading.NavigateUrl = page.LinkURL;
 
		    ltlText.Text = GetText(page, 255);
		}
	    }
	}
    }
}

The GetText method I usually place in a static helper class. It is just to get 255 characters of text from the pages MainIntro property or if it is empty from the MainBody property:

public static string GetText(PageData page, int textLength)
{
    if (page == null)
    {
	return string.Empty;
    }
 
    var text = page["MainIntro"] as string;
 
    if (string.IsNullOrEmpty(text))
    {
	text = page["MainBody"] as string;
    }
 
    if (string.IsNullOrEmpty(text))
    {
	return string.Empty;
    }
 
    // using EPiServer.Core.Html;
    return TextIndexer.StripHtml(text, textLength);
}

You could also add it as an extension method:

// Simply add the this keyword in front of PageData
public static string GetText(this PageData page, int textLength)
{
...
}

Then you can call it like this instead:

ltlText.Text = page.GetText(255);

© Copyright Frederik Vig. Based on Fluid Blue theme