Frederik Vig – ASP.NET developer

Follow me

Archive for the ‘EPiServer’ 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.

Getting the Page and EPiServer CurrentPage object from HttpContext

Just a little quick tip when needing to use either the Page object or the EPiServer CurrentPage object from a class file. HttpContext.Current will give you access to the current request, what we can do is cast HttpContext.Current.Handler (since Page implements the IHttpHandler interface) to System.Web.UI.Page.

var page = HttpContext.Current.Handler as System.Web.UI.Page;
 
if (page != null)
{
    // do something with the page object
}

We can take this a step further and cast it to EPiServer.PageBase which gives us access to the CurrentPage object.

private static PageData CurrentPage
{
    get
    {
	var page = HttpContext.Current.Handler as EPiServer.PageBase;
 
	if (page == null)
	{
	    return null;
	}
 
	return page.CurrentPage;
    }
}

Visual Studio 2010 EPiServer Snippets

I finally got my hands on a copy of Visual Studio 2010 RC1! After playing around a bit, I stumbled across the new snippet functionality in Visual Studio 2010. You can now use snippets in the markup files as well (in previous versions you could only use the snippet functionality in code files like class/interfaces/code-behind files etc). This is very cool, and Microsoft has even included a few snippets for their ASP.NET controls and for HTML elements like: a, table, img, div etc. Quite a time saver!

To test the snippets out, simply type the shortcut (eg. ‘a’ or ‘table’) and press tab.

Visual Studio 2010 table snippet

Visual Studio 2010 table snippet

This is a simple, but very cool feature. Imaging all the typing you can get rid off!

Creating your own snippets

In Visual Studio 2010, under the Tools menu, you’ll find the Code Snippets Manager (ctrl+k, ctrl+b).

Visual Studio 2010 Code Snippets Manager

Here you can add new folders that contain your custom snippets, or check out the other snippets added by Microsoft. You’ll also see the path to the snippets folder (C:\Program Files (x86)\Microsoft Visual Studio 10.0\Web\Snippets\HTML\1033\ in my case). If you open up that folder in Windows Explorer you should see at least two folders there, ASP.NET and HTML. Inside both of these folders you’ll find the snippets for the ASP.NET web controls and for HTML elements. We can open up one of the files and take a look at the code.

<CodeSnippet Format="1.1.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <Header>
    <Title>image</Title>
    <Author>Microsoft Corporation</Author>
    <Shortcut>image</Shortcut>
    <AlternativeShortcuts>
      <Shortcut>imagebutton</Shortcut>
      <Shortcut Value="image">asp:image</Shortcut>
      <Shortcut Value="imagebutton">asp:imagebutton</Shortcut>
    </AlternativeShortcuts>
    <Description>Markup snippet for a control that contains an image</Description>
    <SnippetTypes>
      <SnippetType>Expansion</SnippetType>
    </SnippetTypes>
  </Header>
  <Snippet>
    <Declarations>
      <Literal>
        <ID>imageurl</ID>
        <ToolTip>imageurl</ToolTip>
        <Default>imageurl</Default>
      </Literal>
    </Declarations>
    <Code Language="html"><![CDATA[<asp:$shortcut$ imageurl="$imageurl$" runat="server" />$end$]]></Code>
  </Snippet>
</CodeSnippet>

Very easy and readable XML markup, that we easily can tweak to our needs. Say we’re working on a project where 90% of the tables we create should have a class of “products”. Lets create a snippet for this, so that we don’t have to type the same thing every time.

Create a new snippet called table-products.snippet, open it up in your favorite code editor and add this code.

<CodeSnippet Format="1.1.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <Header>
    <Title>table</Title>
    <Author>Frederik Vig</Author>
    <Shortcut>tablep</Shortcut>
    <Description>Markup snippet for a table with class products</Description>
    <SnippetTypes>
      <SnippetType>Expansion</SnippetType>
      <SnippetType>SurroundsWith</SnippetType>
    </SnippetTypes>
  </Header>
  <Snippet>
    <Declarations>
      <Literal>
        <ID>cellspacing</ID>
        <ToolTip>cellspacing</ToolTip>
        <Default>0</Default>
      </Literal>
    </Declarations>
    <Code Language="html"><![CDATA[<table cellspacing="$cellspacing$" class="products">
    <tr>
        <td>$selected$$end$</td>
    </tr>
</table>]]></Code>
  </Snippet>
</CodeSnippet>

I’ve saved table-products.snippet directly in the HTML snippet folder (C:\Program Files (x86)\Microsoft Visual Studio 10.0\Web\Snippets\HTML\1033\HTML in my case), but you can save it anywhere, just remember to add the snippet folder with the Code Snippets Manager in Visual Studio.

We can now test the snippet by typing tablep and pressing tab.

Visual Studio 2010 table products snippet

Visual Studio 2010 table products snippet

Simple example, I know, but you get the idea!.

EPiServer

Naturally, one of the first things I did was add the EPiServer Property web control to the snippet manager ;) . This turned out to be very easy! So I continued on adding snippets for the other EPiServer web controls as well. Here is the list of snippets with their shortcut.

Download the snippets

EPiServer code walkthrough #1 – 404 handler

This is the first post in a new series called “EPiServer code walkthrough”. What I’ll do is go through one new EPiServer module in each post. Writing a little about what it does, learn by reading its code, and hopefully contributing a little back to the project, if I see any bugs or harmful/unnecessary code that is :) .

I’m a firm believer that reading code helps you become a better developer. There are lots of modules even I just recently came across, that I didn’t know existed just a few weeks back. I think I’m not the only one, especially since they are spread around on various blogs, CodePlex, EPiCode and other places. They all contain valuable code that we as EPiServer developers should read.

I’m starting with a module I personally have not used much before, but that I know a lot of people have used in their projects: the custom 404 handler.

From the project wiki page

This module has a more advanced url redirect feature than the built-in shortcut url feature in EPiServer. It handles extensions and querystring parameters too. If you have a lot of 404 errors in your logs, you can use this to redirect the user to the correct page. Especially useful if you move templates or pages around, or have just installed EPiServer and have a lot of old urls that is no longer available.

404 handlers wiki page explains in detail what you need to do to install the module, so I’m not going to repeat it here. Instead we’ll take a look at the code.

Start by doing a SVN checkout of the source code (the svn url is https://www.coderesort.com/svn/epicode/BVNetwork.404Handler/5.x/).

SVN checkout of 404 handler

When opening up the BVNetwork.404Handler folder you’ll see a readme.txt file explaining the purpose of the module, installation, configuration etc. This is something I like! Not everyone reads the wiki page, and we as developers are especially lazy when it comes to reading documentation. Having a readme.txt with everything you need makes this so much easier, and gives the module author less support questions to answer. When using a new module, always make sure to read the documentation before asking the author questions. Saves both parties time :) .

Lets open up BVNetwork.404Handler.csproj in Visual Studio.

404 handler project structure in Visual Studio

As you can see the project is structured pretty nice, with good separations between the different parts. Very good that the project uses languages files, and that they’ve included them for five different languages. Not all modules use the language files, and if they do, they seldom have any more than one update-to-date English language file. Kudos for this.

What I don’t like is the missing fnf_logo.gif image file in the Images folder. A quick search in Visual Studio shows that this file is no longer in use, and should therefore be removed from the project. A missing image file is not the end of the world, and I know people make mistakes/forget to commit all their files, but this can be very annoying and even lead to a lot of work for other developers using your code or taking over a project.

Always make sure that your project builds on other computers than your own. I recommend manually setting up the project on preferably a new computer to see that everything works as expected, you can also use the same computer, but make sure to do the normal process a new developer would use when setting up the project (new checkout etc). Especially on larger projects that have quite a few things to setup you should do this. You’ll also receive some valuable feedback if your setup process is to complex (again on larger projects this happens quite frequently). Another thing you should use is a build server. There are quite a few out there, I know CodeResort just added Bitten as their build server for CodeResort projects. In a nutshell what a build server does is make sure you’ve included all the files and resources needed to build your project, run various tests (if you have any), deploy your files, create reports, notify team members if their build fails, and a bunch of other stuff, automatically for you. I’ll try to write a blog post up with more information on build and continuous integration servers.

Lets get back to the Visual Studio and the project! One of the things you should start out with is actually making sure the project builds after doing a checkout. So, either press ctrl + shift + b or go to Build -> Build BVNetwork.404Handler. You should have a successful build.

If we take a look through the code we see that most of the code is well documented and follows a nice naming convention making it easy to know what the purpose of the class is (CustomRedirectHandler.cs, CustomRedirectCollection.cs, Custom404Handler.cs etc). But we also see a few minor things we can clean up: unused using statements, commented out code, unused field variables etc. These are minor, but still important things to get rid of. Always strive for a clean code base, commented out code is something you should never commit into a repository, the whole reason for having a source control system is so that you can look at previous changesets and their code, there is no reason to keep the commented out code there, it will only be forgotten by the developer who commented out the code, and the other developers don’t know what to do with it, and will most likely just let it be. When I come across this I show no mercy and just delete it!

You’ll also see a few System.Diagnostics.Debug.WriteLine that are commented out. The project now uses Log4Net to log errors and warnings, System.Diagnostics.Debug.WriteLine is legacy code that we can safely remove. Another thing that I prefer is using string.Empty(); instead of “”, I’ve not updated the code because I feel this is more a personal preference. Same thing when it comes to over-documenting-code, like in this example.

/// <summary>
/// The refering url
/// </summary>
public static string GetReferer(System.Web.UI.Page page)
{
    string referer = page.Request.ServerVariables["HTTP_REFERER"];
    if (referer != null)
    {
	// Strip away host name in front, if local redirect
	string hostUrl = EPiServer.Configuration.Settings.Instance.SiteUrl.ToString();
	if (referer.StartsWith(hostUrl))
	    referer = referer.Remove(0, hostUrl.Length);
    }
    else
	referer = ""; // Can't have null
    return referer;
}

The document header can safely be removed since the method name does a good job at describing what the purpose of the method is. I’ve not removed the document header, since again this is my personal preference.

A thing I just noticed is that this is actually a Visual Studio class project, and not a Web Application project. I can only guess, but this is probably a project that got upgraded from Visual Studio 2003, I’ve previously had some problem upgrading a project from Visual Studio 2003 to Visual Studio 2008. The conversion usually goes okay (just an update to the csproj file mostly), but the ProjectTypeGuids doesn’t always get added. This is easy to fix, open up BVNetwork.404Handler.csproj in notepad or another source editor, and add this line.

<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

Save, and open up the project in Visual Studio. You should now see a slightly different icon at the top of the solution explorer window.

New project icon in Visual Studio

This will allow us to add a designer file to default.aspx from the admin folder. To do this simply right-click the file in solution explorer and choose “Convert to Web Application”. We can now delete the “Web Form Designer generated code”-region.

After all these updates its very important to make sure that the project still builds and that everything functions as expected by testing it.

What I liked the most with this project is how it uses logging. I’m personally very bad at using logging, and know that not all projects use logging as much as they should. Kudos to the authors for this!

I hope you’ve learned a few new things while reading this code, and be sure to try out the 404 handler! For SEO and user experience purposes it is crucial that the users have a smooth transition. They shouldn’t even notice the change when switching to EPiServer from another CMS (that uses different urls). A 404 (file not found) error message can also quickly become a major leak of traffic for your clients site.

A developer’s guide to PageTypeBuilder

Page Type Builder allows developers to define EPiServer page types in code which eliminates the need to synchronize page types between different servers. As page types are declared in code it also enables inheritance between page types and strongly typed property access.

From the PageTypeBuilder project

Joel Abrahamsson has done a superb job with the project and is continually working on it. I’ve earlier blogged a little about the project in my Create an EPiServer site from scratch series, but not done a proper walk-through.

New page type

Creating a new page type is very easy. You create a new class, inherit from TypedPageData, and add the [PageType] attribute to the class declaration (living inside the PageTypeBuilder namespace). You have properties for setting the different page type settings.

  • AvailableInEditMode – bool (default true)
  • AvailablePageTypes – Type[]
  • DefaultArchiveToPageID – int (default -1)
  • DefaultChildSortOrder – FilterSortOrder
  • DefaultFrameID – int
  • DefaultPageName – string
  • DefaultSortIndex – int (default -1)
  • DefaultStartPublishOffsetMinutes – int
  • DefaultStopPublishOffsetMinutes – int
  • DefaultVisibleInMenu – bool (default true)
  • Description – string
  • Filename – string
  • Name – string
  • SortOrder – string (default 100)

Example

using PageTypeBuilder;
 
namespace EPiServer.Templates.Public.PageTypes
{
    [PageType(Name = "[Public] Document list", SortOrder = 1140, Filename = "/Templates/Public/Pages/DocumentList.aspx", AvailablePageTypes = new [] { typeof(Document)})]
    public class DocumentList : TypedPageData
    {
    }
}

For a nice overview of how these properties map to the UI in EPiServer admin mode see: How the PageTypeAttribute’s Properties Map to EPiServer CMS’ Admin

Adding properties

You add properties to a page type by adding new public virtual properties to the class with the [PageTypeProperty] attribute. You have properties for setting the various EPiServer property settings.

  • ClearAllLongStringSettings – bool
  • DefaultValue – string
  • DefaultValueType – DefaultValueType
  • DisplayInEditMode – bool (default true)
  • EditCaption – string
  • HelpText – string
  • LongStringSettings – EditorToolOption
  • Required – bool
  • Searchable – bool
  • SortOrder – int
  • Tab – Type
  • Type – Type
  • UniqueValuePerLanguage – bool

Example

[PageTypeProperty(
    EditCaption = "Secondary body",
    HelpText = "The contents of this property will be shown in the right column of the page, you can use both text and images for layout. Note that images should not be larger than the right area.",
    Searchable = true,
    UniqueValuePerLanguage = true,
    Type = typeof(PropertyXhtmlString),
    LongStringSettings = (
	EditorToolOption.All ^
	EditorToolOption.Font),
    Tab = typeof(Advanced))]
public virtual string SecondaryBody
{
    get;
    set;
}

This will add a new property named SecondaryBody of type PropertyXhtmlString (XHTML string (>255)), with all editor options available, except for font. Use ^ for not available, or just add all the options you like with | between.

LongStringSettings = (EditorToolOption.DynamicContent | EditorToolOption.ToggleHtml)

Only DynamicContent and ToggleHtml will now be available for editors.

UniqueValuePerLanguage = false

When setting UniqueValuePerLanguage to false, and using the auto get setter property (like above), you’ll get null values returned on properties in other languages than the master language. To fix this you can use the GetPropertyValue extension method.

[PageTypeProperty(UniqueValuePerLanguage = false)]
public string MyProperty
{
    get 
    { 
        return this.GetPropertyValue(page => page.MyProperty, true) 
    }
}

You should now get the proper value in all languages, not just in the master language.

Tabs

To create a new tab, create a new class and inherit from Tab (PageTypeBuilder.Tab). You now need to implement three abstract properties

  • Name – string
  • RequiredAccess – AccessLevel
  • SortIndex – int

Example

using EPiServer.Security;
using PageTypeBuilder;
 
namespace EPiServer.Templates.Public.Tabs
{
    public class Advanced : Tab
    {
        public override string Name
        {
            get { return "Advanced"; }
        }
 
        public override AccessLevel RequiredAccess
        {
            get { return AccessLevel.Edit; }
        }
 
        public override int SortIndex
        {
            get { return 20; }
        }
    }
}

You can now add properties to this tab by setting the PageTypeProperty’s Tab property.

[PageTypeProperty(Tab = typeof(Advanced))]
public virtual string MyProperty
{
    get; set;
}

Dynamic properties

At the moment you cannot create dynamic properties with PageTypeBuilder. You can however access them.

public virtual string MetaDescription
{
    get
    {
	return this.GetPropertyValue(page => page.MetaDescription, true);
    }
}

As you’ll see when going through EPiServer public templates with PageTypeBuilder code, I’ve added the dynamic properties to a base class that the other page types inherit from. That way all page types have access to the dynamic properties.

Also be sure to read: Working with Dynamic Properties and Page Type Builder.

Accessing properties in templates

One of the great things about the PageTypeBuilder is that you not get strongly typed access to your properties.

Web forms (.aspx)

Inherit from the generic TemplatePage (living inside the PageTypeBuilder.UI namespace), where the type is set to the page type this template is for.

using PageTypeBuilder.UI;
 
namespace EPiServer.Templates.Public.Pages
{
    public partial class Document : TemplatePage<PageTypes.Document>
    {
    }
}

When typing CurrentPage. you’ll get access to all the public properties that Document exposes. CurrentPage.SecondaryBody for instance.

User controls (.ascx)

Inherit from the generic UserControlBase (living inside the PageTypeBuilder.UI namespace), where the type is set to the page type this user control is for. If the user control is used by multiple page types, simply find a common class to set as the type (a base class).

using PageTypeBuilder.UI;
 
namespace EPiServer.Templates.Public.Units.Placeable
{
    public partial class Document : UserControlBase<PageTypes.BasePageData>
    {
    }
}

Same as with web forms, you’ll get access the the properties that BasePageData exposes.

EPiServer public templates with PageTypeBuilder

I’ve started updating the EPiServer public templates to use PageTypeBuilder. This is not a complete conversion, but merely a nice starting place for you to see how I’ve structured my files and how the code looks when used with an existing project.

Download the code

Other resources

Part 8: Preparing for launch – Create an EPiServer site from scratch

This is the last part of Create an EPiServer site from scratch. We’ve come a long way, almost finishing our site! What we have left is doing some testing and deploying it to our production server.

Client side performance testing

When doing performance testing, we as ASP.NET developers, mostly focus on the server side stuff. EPiServer has done a superb job with making sure that EPiServer looks after server side performance for you, and gives you the tools to improve it even more. But one area of performance we don’t necessarily focus so much on is client side performance.

With client side performance I mean how fast the transfer to the client (user) is. Most of the users time will not be spent waiting for a SQL query to run, but actually downloading all the sites resources (images, css, JavaScript files etc).

There are some easy steps we can take to improve our site even more. Lets start by taking a look at Yahoo!’s Best Practices rules for Speeding up our site. There are some very good rules here that we’ll implement. Yahoo! has also developed a Firefox extension YSlow for Firebug.

YSlow

Install YSlow, and run it by pressing the YSlow button in the bottom right corner of Firefox.

YSlow in Firefox

Click the YSlow tab and Run Test.

YSlow result

Overall we have a result of C, which is fine, but as you can see we have a few F’s around.

ETags

From YSlow rules

Entity tags (ETags) are a mechanism web servers and the browser use to determine whether a component in the browser’s cache matches one on the origin server. Since ETags are typically constructed using attributes that make them unique to a specific server hosting a site, the tags will not match when a browser gets the original component from one server and later tries to validate that component on a different server.

To remove the ETags in IIS 7, open up the IIS Manager, go to the Jungle site, and click HTTP Response Headers, click Add and type ETag into the name and “” into the Value field.

Remove ETags in IIS 7

Expires HTTP headers

Per Bjurström has written on this before, Configuring cache expiration on IIS 7.

I updated the staticContent sections clientCache to.

<staticContent>
    <clientCache cacheControlMode="UseExpires" httpExpires="Sun, 29 Mar 2020 00:00:00 GMT" />
</staticContent>

Which sets the expiration date to 2020 on static content (CSS, JavaScript, PNGs etc).

Expires header with Network monitor in Firebug

If your using IIS 6, you can use a custom static file handler.

using System;
using System.Web;
 
namespace Jungle
{
    public class JungleStaticFileHandler : EPiServer.Web.StaticFileHandler
    {
        protected override void SetCachePolicy(HttpContext context, DateTime fileChangedDate)
        {
            if (fileChangedDate > DateTime.UtcNow)
            {
                fileChangedDate = DateTime.UtcNow;
            }
            context.Response.Cache.SetLastModified(fileChangedDate);
            context.Response.Cache.SetCacheability(HttpCacheability.Public);
            context.Response.Cache.SetExpires(DateTime.UtcNow.AddYears(1));
            context.Response.Cache.SetValidUntilExpires(true);
            context.Response.Cache.VaryByParams.IgnoreParams = true;
            context.Response.Cache.SetOmitVaryStar(true);
        }
    }
}

For some reason you can only add 1 year when doing this with a custom handler. You also need to update the web.config file to use JungleStaticFileHandler instead of StaticFileHandler.

CDN Support

Per Bjurström has another terrific post explaining how to enable CDN for EPiServer. I’ve not enabled it for my local site, but if you do, just remember that you don’t need to use JungleStaticFileHandler anymore, since CdnStaticFileHandler does the same thing.

Fewer HTTP requests

We can easily reduce our HTTP requests by combining our CSS or JavaScript files into one, and by using CSS Sprites. To help with the CSS Sprites there are quite a few generators out there. I encourage you to take a look and try it out, this is one of the most important steps you can take for better client side performance.

Compressing CSS and JavaScript

To compress CSS and JavaScript code we can add either add an HTTP Handler that does this for us, use an online compressor, or add a build task to Visual Studio.

There are a number of resources to help you with compressing CSS and JavaScript files, many of them also combine CSS or JavaScript files into one.

From local development to remote server

We’re now ready to deploy our application to a remote server. Open up UnleashIt (install it if you haven’t done so already).

Create a new profile by going to: File, Profiles, Profile Configuration. Select the Quick Deploy Profile, duplicate it, and give it a name of Jungle. Click OK, and go back to the main UnleashIt window. Under Source destination select your wwwroot folder and choose a destination folder.

UnleashIt profile

If this is the first time you use UnleashIt you need to add a few File Masks (files UnleashIt copies over to the deployment folder). To add new ones go back to Profile Configuration (File, Profiles, Profile Configuration). You have a text field where you can type new file masks, add them by clicking the Add as file mask button.

My file masks

  • *.asax
  • *.ascx
  • *.ashx
  • *.asmx
  • *.aspx
  • *.browser
  • *.config
  • *.css
  • *.dll
  • *.gif
  • *.htm
  • *.html
  • *.ico
  • *.jpg
  • *.js
  • *.master
  • *.png
  • *.skin
  • *.txt
  • *.xml

Database

Take a backup of your development database by using the SQL Server Management Studio (can be installed with Web Platform installer). Right-click the database, and choose Tasks, Back Up (make sure the backup type is set to full).

Database backup with SQL Server Management Studio

Connect to your production database server/instance, create a new database and restore the database by going to: Tasks, Restore, Database. Select from Device and choose our database (dbJungle.bak in my case), check the checkbox next to the backup set to restore. Go to Options and check the checkbox next to Overwrite the existing database, also update the Restore the database files as paths.

Great, we can now click OK, and the restore will start. After you’ve restored successfully, remember to add a user to the db_owner schema. Expand the the database and choose Security, Users, and either add a new one or update the existing one.

Add user to database

Remote server

In my case, I’m deploying the application to a Windows 2008 Server with IIS 7 (same IIS version as my development one).

Either use remote desktop or login to your remote server in another way. Make sure to install EPiServer on the server if it isn’t already installed (see Part 1: Setting up the development environment for more information).

Copy the deployed files to the remote server, I usually use a folder system like this on the remote server: Customers\CustomerName\Internet(Intranet)\wwwroot. Also make sure to copy the VPP folders.

Open up the IIS Manager and create a new application pool by going to Application Pools and Add Application Pool, use the default settings (.NET 2, Integrated mode etc).

Now we can create the site in IIS, right-click Sites, and Add Web Site, give it a name and choose our newly created application pool, choose the wwwroot folder, under host names add the host name you wish to use.

New Site in IIS 7

We need to update connectionStrings.config with the new connection information (new database, user name, password etc), and web.config with the new VPP paths. Just search for VPP and the first hit should match the PageFiles path (there are three paths to update). Last step is updating siteUrl under siteSettings, to our new host name.

You should now have a running site! Test by clicking a little around to make sure everything works as expected.

Sometimes it takes a while for the DNS of the new domain to get updated everywhere, a little trick is to update your local hosts file (C:\Windows\System32\drivers\etc\hosts), and add the host name with the remote servers IP address.

Uptime

There are various tools, both free and commercial, that’ll tell you when a site goes down. One I’ve used in the past is the free BasicState, you also have Pingdom and Uptrends both are commercial. I recommend giving them a try!

Logging exceptions

ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable. It can be dynamically added to a running ASP.NET web application, or even all ASP.NET web applications on a machine, without any need for re-compilation or re-deployment.

ELMAH is a free and easy to use and setup, and you can subscribe to a RSS feed for updates.

ASP.NET Exception Reporter

Another free tool that is based on ELMAH is ASP.NET Exception Reporter.

ASP.NET Exception Reporter (based on ELMAH) is pluggable application-wide error logging facility. Plug it in to your existing webapps, webservices, …
The goal is to centralize the different exceptions that are generated by ELMAH to one web application.
It is developed in C#.

Great for when you need to monitor multiple applications, not just the one.

ASP.NET Exception Reporter

Conclusion

There are of course numerous tasks and functionality we didn’t do or add in this series. I’ve deliberately kept things as simple as possible, and instead tried to give you pointers on how you might extend the site even more. I highly recommend playing around more with the site, and implementing some of the functionality we didn’t implement. Both the footer and the thumbnail images, are a great place to start.

I hope you’ve enjoyed this series, and do be sure to come back to this site for further updates and new blog posts!

Thanks for reading! And merry Christmas! :)

Part 7: Creating the Sitemap page – Create an EPiServer site from scratch

We’re soon ready to launch or site. One thing that is missing is the Sitemap page. This is a page that will help our users find what they’re looking for, and give them a nice overview of the site structure.

Create a new page type with these settings and properties.

using EPiServer.Core;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(
        Name = "[Jungle] Sitemap page",
        Filename = "/Templates/Jungle/Pages/Sitemap.aspx",
        SortOrder = 1060)]
    public class SitemapPage : EditorialPage
    {
        [PageTypeProperty(
            EditCaption = "Sitemap root page",
            Type = typeof(PropertyPageReference))]
        public virtual PageReference SitemapRoot
        {
            get;
            set;
        }
    }
}

We can now create Sitemap.aspx. I’m using a PageTree control for generating the site structure (nested ordered lists).

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Sitemap.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.Sitemap" MasterPageFile="~/Templates/Jungle/MasterPages/Site.Master" %>
<%@ Register TagPrefix="Jungle" TagName="MainBody" Src="~/Templates/Jungle/Units/MainBody.ascx" %>
<asp:Content ContentPlaceHolderID="MainContentRegion" runat="server">
 
    <EPiServer:PageTree runat="server" ExpandAll="true" ID="SitemapTree">
        <HeaderTemplate>
            <div id="Sitemap">
        </HeaderTemplate>
        <IndentTemplate>
            <ol>
        </IndentTemplate>
        <ItemHeaderTemplate>
            <li>
        </ItemHeaderTemplate>
        <ItemTemplate>
            <EPiServer:Property PropertyName="PageLink" runat="server" />
        </ItemTemplate>
        <ItemFooterTemplate>
            </li>
        </ItemFooterTemplate>
        <UnindentTemplate>
            </ol>
        </UnindentTemplate>
        <FooterTemplate>
            </div>
        </FooterTemplate>
    </EPiServer:PageTree>
</asp:Content>

Notice ExpandAll=”true” property, this will expand all levels. The code-behind just gets the start position from the SitemapRoot property, if its not set it uses the start page as the start position.

using EPiServer.Core;
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Pages
{
    public partial class Sitemap : TemplatePage<SitemapPage>
    {
        protected override void OnLoad(System.EventArgs e)
        {
            base.OnLoad(e);
 
            this.SitemapTree.PageLink = CurrentPage.SitemapRoot ?? PageReference.StartPage;
            this.SitemapTree.DataBind();
        }
    }
}

Build, and go to edit mode, and create a new Sitemap page. Make sure that everything works.

Sitemap page

This is a very common and standard sitemap page. Earlier the sitemap page was used by search engines for having a link to every page on the site. Fortunately Google, Microsoft, Yahoo! got together to create a Sitemap XML standard that the most popular search engines use. On EPiCode we have the Search Engine Sitemap module that creates an XML file according to the Sitemap XML standard for EPiServer sites. I recommend using it on all your sites for some free SEO boost.

After the introduction of the Search Engine Sitemap we have had some change on what the purpose and usage of the sitemap for a site would be. I’ve seen a lot of great sitemaps that do a much better job when it comes to usability than the traditional sitemap. One I like very much is Washtenaw Community College service index.

In this series I’ve tried keeping things simple, to give you a foundation that you can build and extend upon. This is especially true for the sitemap page type. I encourage you to extend it with your own code and functionality, making it even more usable.

Cleaning up

We’re using MainBody.ascx on most of our page templates. Some of the functionality we only need for Page.aspx. Lets create a new user control with the name ArticleMainBody.ascx, and copy paste everything from MainBody (except the first line), into ArticleMainBody.ascx. Update Page.aspx to use ArticleMainBody.ascx instead.

Lets remove most of the code from MainBody.ascx, we should have something like this left.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="MainBody.ascx.cs" Inherits="Jungle.Templates.Jungle.Units.MainBody" %>
<h2><%= CurrentPage.Heading() %></h2>
<EPiServer:Property runat="server" PropertyName="MainBody" DisplayMissingMessage="false" EnableViewState="false" />
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Units
{
    public partial class MainBody : UserControlBase<BasePageData>
    {
    }
}

Build and make sure everything works.

Refactoring

Always try to improve and refactor your code along the way. I usually make it work, and then refactor my code. If you write the same code twice, try to take out the common code and place it somewhere where both can use it, either in a base class, a helper util class, extension method or something else.

I recommend checking out the 31 Days of Refactoring series and the book Professional Refactoring in C# & ASP.NET, you also have the classic Refactoring: Improving the Design of Existing Code.

In the last post in this series we’ll prepare for launching the site.

Part 6: Creating the XForm page – Create an EPiServer site from scratch

The XForm page will help our editors collect data from their users. Editors can easily customize their forms the way that they want, and decide how to store the data (in a database, by email etc).

Lets create a new page type.

using EPiServer.Core;
using EPiServer.XForms;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(
        Name = "[Jungle] XForm page",
        Filename = "/Templates/Jungle/Pages/Xform.aspx",
        SortOrder = 1020)]
    public class XFormPage : EditorialPage
    {
        [PageTypeProperty(EditCaption = "XForm", Searchable = false, Type = typeof(PropertyXForm))]
        public virtual XForm XForm
        {
            get;
            set;
        }
    }
}

Like with StandardPage and SearchPage, we derive from EditorialPage, with the Heading and MainBody properties. We only have to add the XForm property for letting the editor choose which XForm she wishes to use.

Lets create the Xform.aspx page in Visual Studio, delete the default markup and add the master page.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="XForm.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.XForm" MasterPageFile="~/Templates/Jungle/MasterPages/Site.Master" %>
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Pages
{
    public partial class XForm : TemplatePage<XFormPage>
    {
 
    }
}

After you’ve done that create a new user control called XFormControl.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="XFormControl.ascx.cs" Inherits="Jungle.Templates.Jungle.Units.XFormControl" %>
<asp:Panel runat="server" CssClass="xForm">
	<XForms:XFormControl ID="FormControl" runat="server" ValidationGroup="XForm" />
</asp:Panel>

The code-behind file has a public property for the XForm EPiServer property. This way we can easily reuse this user control in various page types. The other thing to note is how we create the form. We have the XFormControl, what we need to do is set its FormDefinition property to our form (which we need to create). The Form is a property that uses the XFormProperty property to get the form guid, which it then uses to create a new XForm instance.

using System;
using EPiServer.XForms;
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Units
{
    public partial class XFormControl : UserControlBase<BasePageData>
    {
        private XForm _form;
 
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
 
            if (this.Form == null)
            {
                return;
            }
 
            FormControl.FormDefinition = this.Form;
        }
 
        public XForm Form
        {
            get
            {
                if (_form == null)
                {
                    string formGuid = CurrentPage[this.XFormProperty] as string;
                    if (!string.IsNullOrEmpty(formGuid))
                    {
                        _form = XForm.CreateInstance(new Guid(formGuid));
                    }
                }
 
                return _form;
            }
            set
            {
                _form = value;
            }
        }
 
        public string XFormProperty
        {
            get;
            set;
        }
    }
}

For more information about the XFormControl see the SDK.

Remember to add the usercontrol to XForm.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="XForm.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.XForm" MasterPageFile="~/Templates/Jungle/MasterPages/Site.Master" %>
<%@ Register TagPrefix="Jungle" TagName="MainBody" Src="~/Templates/Jungle/Units/MainBody.ascx" %>
<%@ Register TagPrefix="Jungle" TagName="XFormControl" Src="~/Templates/Jungle/Units/XFormControl.ascx" %>
 
<asp:Content ContentPlaceHolderID="MainContentRegion" runat="server">
    <Jungle:MainBody runat="server" />
 
    <Jungle:XFormControl
            runat="server" 
            XFormProperty="XForm" />
</asp:Content>

Build and create a new XForm page with a test form.

Confirmation email

So far so good, but we also want to send a confirmation email to the user when they’ve sent the form. Just a thank you for your feedback message, or something similar. I’ve written about this previously: Sending confirmation email to the user when using EPiServer XForms. Read the blog post or download the code to see how I’ve implemented it.

One thing I’ve done, that differs from the original post, is that I’ve added a property that lets the editor edit the email text. For this I added a new property to [Jungle] XForm page:

[PageTypeProperty(
    EditCaption = "Confirmation email text",
    Searchable = true,
    Type = typeof(PropertyXhtmlString),
    LongStringSettings = (
	EditorToolOption.All ^
	EditorToolOption.Font))]
public virtual string ConfirmationEmailText
{
    get;
    set;
}

Enter key

A problem we’re facing is when users are filling out our form, and use the enter key. Instead of sending the form back to the server, they’re actually triggering the quick search button instead. The reason for this behavior is that ASP.NET web forms just uses one form for everything. Unfortunately there are no good fixes for this. One that I’ve used in the past is adding some JavaScript that detects if the enter key is pressed, and then triggers the first submit buttons click event.

The jQuery code for this:

$(function() {
    $('#id_matrix input').keypress(function(e) {
        if (e.keyCode && e.keyCode == 13) {
            $('#id_matrix :submit:first').click();
            return false;
        } else {
            return true;
        }
    });
});

You can of course modify the code to look for a button with the class “default” instead. But in my experience editors tend to forget this little detail, and they mostly only have one button anyway.

I’ve placed this code in an external JavaScript file, and added some code in XFormControl.ascx to add it.

protected override void OnInit(EventArgs e)
{
    ...
 
    if (!Page.ClientScript.IsClientScriptIncludeRegistered("Xform"))
    {
	Page.ClientScript.RegisterClientScriptInclude("Xform", Page.ResolveUrl("~/Templates/Jungle/Scripts/Xform.js"));
    }
}

Our users can now use the enter key! The drawback is that the submit button must be the first button, and that this only works when users have JavaScript enabled.

Great! We now have our form in place. Next up is Part 7: Creating the Sitemap page.

Part 5: Creating the search page – Create an EPiServer site from scratch

Today we’re implementing our sites search page. This will be a very standard search page with nothing fancy. Instead I’ve included some links that’ll help you extend this search page even more.

Quick search

We already have the markup for the quick search box placed in quicksearch.ascx, we’ll start by converting it to use ASP.NET controls instead.

Open up quicksearch.ascx. Lets replace the form element with a fieldset and give it the same id. Replace the input text element with an ASP.NET TextBox control and the input submit element with an ImageButton control. You should now have something like this.

<fieldset id="quick-search">
	<p>
	<label for="qsearch">Search:</label>
	<asp:TextBox CssClass="tbox" runat="server" ID="txtQuickSearch" ToolTip="Start typing and hit ENTER" Text="Search..." />
	<asp:ImageButton runat="server" CssClass="btn" ID="btnQuickSearch" CausesValidation="false" ToolTip="Search" ImageUrl="/Templates/Jungle/images/search.png" />  
	</p>
</fieldset>

Notice the CausesValidation=”false” on the ImageButton. The reason for this is that ASP.NET Validation controls that are used on the page would otherwise get triggered when the user uses our quicksearch button. Setting the CausesValidation property to false bypasses all validation.

Next we need to fix the label, and make sure that our users can use the enter key after filling out a search phrase. The reason that this doesn’t work out of the box in ASP.NET Web forms is that everything is placed inside one form, which means that only one button can be the default on. Though as you’ll see we can fix this for users that have JavaScript enabled.

<asp:Panel runat="server" DefaultButton="btnQuickSearch">
<fieldset id="quick-search">
	<p>
	<asp:Label runat="server" AssociatedControlID="txtQuickSearch">Search:</asp:Label>
	<asp:TextBox CssClass="tbox" runat="server" ID="txtQuickSearch" ToolTip="Start typing and hit ENTER" Text="Search..." />
	<asp:ImageButton runat="server" CssClass="btn" ID="btnQuickSearch" CausesValidation="false" ToolTip="Search" ImageUrl="/Templates/Jungle/images/search.png" />  
	</p>
</fieldset>
</asp:Panel>

For the enter key issue I added a Panel control and set its DefaultButton to the ImageButton’s ID. This will add the necessary JavaScript code for us. For the label I added a Label control and sat its AssociatedControlID to the TextBox’s ID. This will render as a HTML Label element and set the for attribute to the correct input’s id (ASP.NET Label controls normally render as span elements).

I also had to update the CSS code in Jungleland.css a little to make the selectors work with the slightly changed markup. I just removed the form prefix (form#quick-search).

/* header quick search */
#header #quick-search {
	position: absolute;
	top: 95px; right: 15px;
	z-index: 999999;
	padding: 0; margin: 0;
	border: none;
	width: 222px; height: 32px;
	background: url(../images/header-search.png) no-repeat;		
}
#header #quick-search p {
	margin: 0; padding: 0;		
	border: none;
}
#header #quick-search input {
	float: left;
	margin: 0; padding: 5px;
	border: none;
	background: transparent;
	color: #4a413c;	
}
#header #quick-search .tbox {
	margin: 6px 0 0 5px; 
	width: 170px;	
	display: inline;		
}
#header #search #quick-search .btn{
	width: 25px; height: 25px;		
}
#header #quick-search label {
	display: none;
}

Lets add a click event method to the button and the necessary code for redirecting the user to our search page.

<asp:ImageButton runat="server" OnClick="btnQuickSearch_Click" CssClass="btn" ID="btnQuickSearch" CausesValidation="false" ToolTip="Search" ImageUrl="/Templates/Jungle/images/search.png" />
using System;
using System.Web;
using EPiCode.Extensions;
using EPiServer.Core;
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Units
{
    public partial class QuickSearch : UserControlBase<BasePageData>
    {
        protected void btnQuickSearch_Click(object sender, EventArgs e)
        {
            var startPage = PageReference.StartPage.GetPage() as StartPage;
 
            if (startPage == null || PageReference.IsNullOrEmpty(startPage.SearchPage))
            {
                return;
            }
 
            PageData searchPage = startPage.SearchPage.GetPage();
 
            if (searchPage != null)
            {
                Response.Redirect(HttpUtility.UrlPathEncode(string.Format("{0}&q={1}", searchPage.LinkURL, txtQuickSearch.Text)));
            }
        }
    }
}

As you can see we’re getting the search page from the dynamic property SearchPage.

I’ve also added suggestion functionality to the quick search field. See my post Extending search field with suggestion box, for more information. All the code is also available on EPiCode.

Quick Search Suggestion in action

Remember to remove the Visible=”false” property from QuickSearch in PageHeader.ascx.

Search page

We can now start creating the search page. Create the Search page type by creating a new class with the name SearchPage, and the proper attribute declaration.

using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(
        Name = "[Jungle] Search page ",
        Filename = "/Templates/Jungle/Pages/Search.aspx",
        SortOrder = 1030)]
    public class SearchPage : BasePageData
    {
    }
}

For this page type we only need three properties. Heading, MainBody, and SearchRoot. Since we already have the code for Heading and MainBody in StandardPage, we’re going to refactor and create a super class that StandardPage and SearchPage can derive from.

using EPiServer.Core;
using EPiServer.Editor;
using EPiServer.SpecializedProperties;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    public abstract class EditorialPage : BasePageData
    {
        [PageTypeProperty(Searchable = true, Type = typeof(PropertyString))]
        public virtual string Heading
        {
            get;
            set;
        }
 
        [PageTypeProperty(
            EditCaption = "Main body",
            Searchable = true,
            Type = typeof(PropertyXhtmlString),
            LongStringSettings = (
                EditorToolOption.All ^
                EditorToolOption.Font))]
        public virtual string MainBody
        {
            get;
            set;
        }
    }
}

We can now update StandardPage to derive from EditorialPage instead of BasePageData, and delete the Heading and MainBody property.

using EPiServer.SpecializedProperties;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(
        Name = "[Jungle] Standard page ",
        Filename = "/Templates/Jungle/Pages/Page.aspx",
        SortOrder = 1010)]
    public class StandardPage : EditorialPage
    {
        [PageTypeProperty(EditCaption = "List image", Type = typeof(PropertyImageUrl))]
        public virtual string ListImage
        {
            get; set;
        }
 
        [PageTypeProperty(EditCaption = "Featured image", Type = typeof(PropertyImageUrl))]
        public virtual string FeaturedNewsImage
        {
            get;
            set;
        }
    }
}

Lets also update SearchPage to derive from EditorialPage and add the SearchRoot property.

using EPiServer.Core;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(
        Name = "[Jungle] Search page ",
        Filename = "/Templates/Jungle/Pages/Search.aspx",
        SortOrder = 1030)]
    public class SearchPage : EditorialPage
    {
        [PageTypeProperty(
            EditCaption = "Search root page",
            Type = typeof(PropertyPageReference))]
        public virtual PageReference SearchRoot
        {
            get;
            set;
        }
    }
}

Go back to edit mode and create the search page, just set the SearchRoot to our start page. Then go to the start page and point the SearchPage dynamic property to the search page (remember to check the checkbox that tells EPiServer to inherit this to sub pages). After you’ve done that make sure that quick search redirects to the correct page.

Search.aspx

We can now create Search.aspx.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Search.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.Search" MasterPageFile="~/Templates/Jungle/MasterPages/Site.Master" %>
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Pages
{
    public partial class Search : TemplatePage<SearchPage>
    {
    }
}

Standard stuff :) .

The code for our search page consists of a TextBox, Button, ListView for the result, some text if there are no results, and a SearchDataSource control for searching.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Search.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.Search" MasterPageFile="~/Templates/Jungle/MasterPages/Site.Master" %>
<%@ Import Namespace="EPiServer.Core"%>
<asp:Content runat="server" ContentPlaceHolderID="MainContentRegion">
    <h2><%= CurrentPage.Heading() %></h2>
    <EPiServer:Property runat="server" PropertyName="MainBody" DisplayMissingMessage="false" EnableViewState="false" />
 
    <div id="SearchArea">
        <asp:Label runat="server" AssociatedControlID="txtSearch" CssClass="accessibility">Search:</asp:Label>
        <asp:TextBox ID="txtSearch" text="Search..." runat="server" />
        <asp:Button ID="btnSearch" CssClass="button" runat="server" OnClick="btnSearch_Click" Text="Search" /><br />
    </div>
 
        <div id="ResultArea">
 
            <asp:ListView ID="SearchResult" DataSourceID="SearchDataSource" runat="server" Visible="false" ItemPlaceholderID="itemPlaceHolder">
                <ItemTemplate>
                    <li>
                        <h3><EPiServer:Property runat="server" PropertyName="PageLink" /></h3>
                        <p>
                             <%# ((PageData)Container.DataItem).PreviewText(150)%>
                        </p>
                    </li>
                </ItemTemplate>
                <LayoutTemplate>
                    <h2>Search result</h2>
                    <ol>
                        <asp:PlaceHolder runat="server" ID="itemPlaceHolder" />
                    </ol>
                </LayoutTemplate>
                <EmptyDataTemplate>
                    <p>No matching results were found</p>
                </EmptyDataTemplate>
            </asp:ListView>
        </div>
 
        <EPiServer:SearchDataSource ID="SearchDataSource" runat="server" EnableVisibleInMenu="false">
        <SelectParameters>
            <EPiServer:PropertyParameter Name="PageLink" PropertyName="SearchRoot" />
            <asp:controlparameter name="SearchQuery" controlid="txtSearch" propertyname="Text"/>
        </SelectParameters>
    </EPiServer:SearchDataSource>
</asp:Content>
using System;
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Pages
{
    public partial class Search : TemplatePage<SearchPage>
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            Form.DefaultButton = btnSearch.UniqueID;
 
            if (!IsPostBack)
            {
                string query = Request.QueryString["q"];
                if (!String.IsNullOrEmpty(query))
                {
                    txtSearch.Text = query;
                    SearchResult.Visible = true;
                }
            }
        }
 
        protected void btnSearch_Click(object sender, EventArgs e)
        {
            SearchResult.Visible = true;
            SearchResult.DataBind();
        }
    }
}

A very simple search page that looks something like this when used.

Search result

The next post: Part 6: Creating the XForm page.

Other resources

Part 4: Creating the standard page – Create an EPiServer site from scratch

In this part we’re going to create the standard page. We’ll use this page type for most of our site, from news to other stuff. As always start by taking a look at what you’re about to implement: style.html. It contains all the common scenarios that editors might use, tables, lists, quotes, links, images etc.

After taking a look at what we’re about to create, we can start planning. Special information includes: title, author name, categories, the main text and a sub menu.

Lets start by creating a new class with the name StandardPage, derive from BasePageData, and add the PageType attribute to the class declaration.

using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(
        Name = "[Jungle] Standard page ",
        Filename = "/Templates/Jungle/Pages/Page.aspx",
        SortOrder = 1010)]
    public class StandardPage : BasePageData
    {
    }
}

Lets now add the four properties: Heading, MainBody, ListImage, and FeaturedNewsImage.

using EPiServer.Core;
using EPiServer.Editor;
using EPiServer.SpecializedProperties;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(
        Name = "[Jungle] Standard page ",
        Filename = "/Templates/Jungle/Pages/Page.aspx",
        SortOrder = 1010)]
    public class StandardPage : BasePageData
    {
        [PageTypeProperty(Searchable = true, Type = typeof(PropertyString))]
        public virtual string Heading
        {
            get;
            set;
        }
 
        [PageTypeProperty(
            EditCaption = "Main body",
            Searchable = true,
            Type = typeof(PropertyXhtmlString),
            LongStringSettings = (
                EditorToolOption.All ^
                EditorToolOption.Font))]
        public virtual string MainBody
        {
            get;
            set;
        }
 
        [PageTypeProperty(EditCaption = "List image", Type = typeof(PropertyImageUrl))]
        public virtual string ListImage
        {
            get; set;
        }
 
        [PageTypeProperty(EditCaption = "Featured image", Type = typeof(PropertyImageUrl))]
        public virtual string FeaturedNewsImage
        {
            get;
            set;
        }
    }
}

Next up is creating the page template in Visual Studio. Right-click Pages, and choose New Item. Choose Web form and give it a name of Page.aspx. Delete all the markup (everything except the first line), and set MasterPageFile to Site.Master.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Page.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.Page" MasterPageFile="~/Templates/Jungle/MasterPages/Site.Master" %>

Open up your code-behind file and derive from TemplatePage<StandardPage> so that we can use our four properties (remember to add using PageTypeBuilder.UI;). Also remove the Page_Load method.

If we build (ctrl + b), go back to edit mode, and create a new page based on our standard page type, we should get something like this.

Something is seriously wrong – this is not how our prototype looked at all. When opening up Firebug and inspecting the form element (surrounding the content), you’ll see some CSS rules that get triggered. They are on line 136 in Jungleland.css, for now just comment out the form and form p selectors.

/* form elements */
/*form {
	margin: 10px 20px 10px 20px; 
	padding: 15px 25px 25px 25px; 
	border: 1px solid #251a14;
	background-color: #130d0a;	
}
form p {
	border-bottom: 1px solid #221813; 
	margin: 0;
	padding: 13px 5px 8px 5px;		
	color: #fff;
}*/

Save, and go back to your browser, you should now see a better result. But still some problems with the quick search field at the top. Lets just hide that for now. Go to PageHeader.ascx and set the QuickSearch’s Visible property to false.

<Jungle:QuickSearch runat="server" Visible="false" />

There we go!

Main body

Create a new user control, MainBody.ascx. We’ll use it to hold the title, author, category, main text and date. The reason for putting this inside a user control and not directly into Page.aspx, is so that we can easily reuse the same functionality in other page types, and more cleanly separate things.

Copy <div class=”post”>, and everything inside it, into MainBody.ascx. Replace the text with the EPiServer Property control, and set the PropertyName to MainBody. Add a new extension method for getting the heading of the page and using the PageName as a fallback.

public static string Heading(this PageData page)
{
    if (page.PageLink == null || page.PageLink.ID < 1)
    {
	return string.Empty;
    }
 
    if (page.IsValue("Heading"))
    {
	return page["Heading"].ToWebString();
    }
 
    return page.PageName.ToWebString();
}

We can also use the Author extension method that we created in the previous post. You should now have something like this.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="MainBody.ascx.cs" Inherits="Jungle.Templates.Jungle.Units.MainBody" %>
<%@ Import Namespace="Jungle.Templates.Jungle.Util"%>
<div class="post">
    <h2><%= CurrentPage.Heading() %></h2>
    <p class="post-info">
        Posted by <a href="index.html"><%= CurrentPage.Author() %></a> | Filed under <a href="index.html">templates</a>,
        <a href="index.html">internet</a></p>
    <EPiServer:Property runat="server" PropertyName="MainBody" DisplayMissingMessage="false" EnableViewState="false" />
    <p class="postmeta">
        <a href="index.html" class="readmore">Read more</a> | <a href="index.html" class="comments">
            Comments (3)</a> | <span class="date">August 20, 2009</span>
    </p>
</div>

One thing that I find annoying is having to add <%@ Import Namespace="Jungle.Templates.Jungle.Util"%> everywhere I’m using something from our util classes. Lets add the namespace to our web.config file instead. We can do the same with EPiCode.Extensions.

<pages validateRequest="false" enableEventValidation="false">
    <controls>
    ...
    </control>
    <namespaces>
	...
	<add namespace="Jungle.Templates.Jungle.Util"/>
	<add namespace="EPiCode.Extensions"/>
    </namespaces>
</pages>

Before testing this new functionality, we need to register MainBody with Page.aspx.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Page.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.Page" MasterPageFile="~/Templates/Jungle/MasterPages/Site.Master" %>
<%@ Register TagPrefix="Jungle" TagName="MainBody" Src="~/Templates/Jungle/Units/MainBody.ascx" %>
 
<asp:Content runat="server" ContentPlaceHolderID="MainContentRegion">
    <Jungle:MainBody runat="server" />
</asp:Content>
 
After testing and making sure we didn't break anything, we can continue implementing. I've added the date of the article and its categories.
<pre lang="csharp">
<div class="post">
    <h2><%= CurrentPage.Heading() %></h2>
    <p class="post-info">
        Posted by <%= CurrentPage.Author() %>| Filed under <%= CurrentPage.GetCategoryNamesSeparated(",") %></p>
    <EPiServer:Property runat="server" PropertyName="MainBody" DisplayMissingMessage="false" EnableViewState="false" />
    <p class="postmeta">
        <span class="date"><%= CurrentPage.StartPublish.ToString("dd MMMM yyyy")%></span>
    </p>
</div>

The GetCategoryNamesSeparated extension method.

public static string GetCategoryNamesSeparated(this PageData page, string separator)
{
    if (page.PageLink == null || page.PageLink.ID < 1)
    {
	return string.Empty;
    }
 
    string result = string.Empty;
 
    int categoryCount = page.Category.Count;
    int categoryNumber = 1;
 
    CategoryList categories = page.Category;
    const string format = "{0}{1} ";
    foreach (var categoryId in categories)
    {
	var category = Category.Find(categoryId);
 
	if (category == null)
	{
	    break;
	}
 
	if (categoryNumber++ == categoryCount)
	{
	    result += string.Format(format, category.Name, string.Empty);
	    break;
	}
 
	result += string.Format(format, category.Name, separator);
    }
 
    return result;
}

After adding some dummy text in edit mode I get something like this.

Standard page with dummy text

Sub menu

I’ve in a previous blog post written about the PageTree control. That post describes it pretty well, so I’m not going to repeat it here. Instead I’ll just show you the code, and encourage you to check out the post for more information.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="SubMenu.ascx.cs" Inherits="Jungle.Templates.Jungle.Units.SubMenu" %>
<EPiServer:PageTree ShowRootPage="false" runat="server" id="Menu">
    <HeaderTemplate>
        <div class="sidemenu">	
	        <h3>Sidebar Menu</h3>
    </HeaderTemplate>
	<IndentTemplate>
		<ul>
	</IndentTemplate>
 
	<ItemHeaderTemplate>
			<li>
	</ItemHeaderTemplate>
 
	<ItemTemplate>
				<EPiServer:Property PropertyName="PageLink" runat="server" />
	</ItemTemplate>
 
	<SelectedItemTemplate>
	            <EPiServer:Property CssClass="current" PropertyName="PageLink" runat="server" />
	</SelectedItemTemplate>
 
	<ItemFooterTemplate>
			</li>
	</ItemFooterTemplate>
 
	<UnindentTemplate>
		</ul>
	</UnindentTemplate>
	<FooterTemplate>
	    </div>
	</FooterTemplate>
</EPiServer:PageTree>
using System;
using EPiServer.Web.WebControls;
using Jungle.Templates.Jungle.PageTypes;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Units
{
    public partial class SubMenu : UserControlBase<BasePageData>
    {
        public MenuList MenuList
        {
            get;
            set;
        }
 
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
 
            if (this.MenuList == null)
            {
                return;
            }
 
            this.Menu.PageLink = MenuList.OpenTopPage;
            this.Menu.DataBind();
        }
    }
}

I also had to update PageHeader.ascx.cs and add a public property so that I could access the main menu

public MainMenu MainMenu
{
    get
    {
	return Menu;
    }
}

We can now register SubMenu in Site.master. We also need to give PageHeader an id, so that we get access it from Site.master.cs.

I added code for setting SubMenu’s MenuList property in Site.masters OnLoad method

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
 
    if (SubMenu != null && PageHeader.MainMenu != null)
    {
	SubMenu.MenuList = PageHeader.MainMenu.MenuList;
    }
 
    var page = (PageBase)Page;
    Page.Title = string.Format(_title, page.CurrentPage.Property["PageName"].ToWebString(), TitleSeparator, EPiServer.Configuration.Settings.Instance.SiteDisplayName);
 
    this.HtmlElement.Attributes["lang"] = page.CurrentPage.LanguageBranch;
}

Build and test that everything works as expected. Also make sure that the functionality we created on the start page works as expected, by creating some dummy pages.

The start page with some dummy content

ShareIt

ShareIt is custom property that lets you easily share an article with social media sites like Twitter, Facebook etc. I already have a post with some background on ShareIt. Be sure to have a look at it.

I added a new property to the start page under the Site configuration tab. To be able to add properties to any other tab than information (default), you have to create a new class with the tab, and derive from PageTypeBuilder’s tab class.

using EPiServer.Security;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.Tabs
{
    public class SiteConfiguration : Tab
    {
        public override string Name
        {
            get { return "SiteConfiguration"; }
        }
 
        public override AccessLevel RequiredAccess
        {
            get { return AccessLevel.Edit; }
        }
 
        public override int SortIndex
        {
            get { return 30; }
        }
    }
}

Notice that I’ve placed SiteConfiguration.cs in a new folder called Tabs (under Templates and Jungle).

We can now add our property to the start page and place it under the SiteConfiguration tab.

[PageTypeProperty(
    Type = typeof(ShareIt),
    Tab = typeof(SiteConfiguration))]
public virtual string ShareIt
{
    get;
    set;
}

The code for displaying ShareIt in MainBody.ascx. Remember to define which social media sites you want to display under the site configuration tab on the start page.

<div class="postmeta">
    <EPiServer:Property runat="server" ID="ShareIt" PropertyName="ShareIt" />
    <span class="date"><%= CurrentPage.StartPublish.ToString("dd MMMM yyyy")%></span>
</div>

Notice that I updated <p class=”postmeta”> to <div class=”postmeta”>

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
 
    this.ShareIt.PageLink = PageReference.StartPage;
}

I also added some CSS code to Jungleland.css.

/* ShareIt */
.shareit {
    clear: both;
    float: right;
}
.shareit ul
{
    overflow: hidden;
    margin: 0;
    padding: 0;
    float: left;
    margin-right: 15px;
}
 
.shareit h4 {
    float: left;
    font-size: 12px;
    margin: 2px;
    margin-top: 3px;
    font-weight: normal;
    color: #91807F;
}
 
.shareit .showmore {
    font-size: 12px;
    color: #000;
    line-height: 20px;
}
 
.shareit li
{
    float: left;
    margin-left: 2px;
    margin-top: 2px;
    list-style: none;
}
 
.shareit li a
{
    text-indent: -9999px;
    display: block;
    width: 16px;
    height: 16px;
    background: url(/FV.ShareIt/Images/all-16x16.png) no-repeat top left;
}
 
.shareit .facebook
{
    background-position: 0 -85px;
}
 
.shareit .feed 
{
    background-position: 0 -102px;
}
 
.shareit .twitter
{
    background-position: 0 -357px;
}
 
.shareit .bebo
{
    background-position: 0 0;
}
 
.shareit .delicious
{
    background-position: 0 -17px;
}
 
.shareit .digg
{
    background-position: 0 -34px;
}
 
.shareit .email
{
    background-position: 0 -68px;
}
 
.shareit .google
{
    background-position: 0 -119px;
}
 
.shareit .linkedin
{
    background-position: 0 -136px;
}
 
.shareit .messenger
{
    background-position: 0 -153px;
}
 
.shareit .mixx
{
    background-position: 0 -170px;
}
 
.shareit .myspace
{
    background-position: 0 -187px;
}
 
.shareit .nettby {
    background-position: 0 -204px;
}
 
.shareit .netvibes
{
    background-position: 0 -221px;
}
 
.shareit .newsvine
{
    background-position: 0 -238px;
}
 
.shareit .pdf {
	background-position: 0 -255px;
}
 
.shareit .print {
	background-position: 0 -272px;
}
 
.shareit .reddit
{
    background-position: 0 -289px;
}
 
.shareit .share
{
    background-position: 0 -306px;
}
 
.shareit .stumbleupon
{
    background-position: 0 -323px;
}
 
.shareit .technorati
{
    background-position: 0 -340px;
}
 
 
.shareit .yahoo
{
    background-position: 0 -374px;
}
 
.shareit .yahoobuzz
{
    background-position: 0 -391px;
}

And the result.

ShareIt

RSS feed

To create RSS feeds we need to add a new page type that allows our editors to choose of what they wish to create a RSS feed of.

using EPiServer.Core;
using PageTypeBuilder;
 
namespace Jungle.Templates.Jungle.PageTypes
{
    [PageType(Name = "[Jungle] RSS feed", Description = "Source of a rss feed", Filename = "/Templates/Jungle/Pages/RssFeed.aspx", SortOrder = 1080)]
    public class RssFeedPage : BasePageData
    {
        [PageTypeProperty(UniqueValuePerLanguage = true, Searchable = true, Type = typeof(PropertyString), EditCaption = "Main heading", HelpText = "This heading will present the main title of this page, if this property is not set the page name will be used as a heading instead.")]
        public virtual string Heading
        {
            get;
            set;
        }
 
        [PageTypeProperty(UniqueValuePerLanguage = true, Searchable = true, Type = typeof(PropertyString), HelpText = "The description of the rss feed")]
        public virtual string Description
        {
            get;
            set;
        }
 
        [PageTypeProperty(UniqueValuePerLanguage = true, Searchable = false, Type = typeof(PropertyPageReference), EditCaption = "Rss source", HelpText = "RSS-source page")]
        public virtual PageReference RssSource
        {
            get;
            set;
        }
    }
}

RssFeed.aspx looks like this.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="RssFeed.aspx.cs" Inherits="Jungle.Templates.Jungle.Pages.RssFeed" %>
using System;
using System.Xml;
using EPiCode.Extensions;
using EPiServer.Core;
using EPiServer.Filters;
using Jungle.Templates.Jungle.PageTypes;
using Jungle.Templates.Jungle.Util;
using PageTypeBuilder.UI;
 
namespace Jungle.Templates.Jungle.Pages
{
    public partial class RssFeed : TemplatePage<RssFeedPage>
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
 
            Response.ContentType = "text/xml";
            Response.Clear();
            if (!PageReference.IsNullOrEmpty(CurrentPage.RssSource))
            {
                this.WriteRssDocument();
            }
 
            Response.End();
        }
 
        private void WriteRssDocument()
        {
            var doc = new XmlDocument();
            XmlNode outerNode = doc.CreateElement("rss");
            XmlAttribute uriInfo = doc.CreateAttribute("xmlns:dc");
            uriInfo.Value = "http://purl.org/dc/elements/1.1/";
            outerNode.Attributes.Append(uriInfo);
            doc.AppendChild(outerNode);
 
            XmlAttribute versionInfo = doc.CreateAttribute("version");
            versionInfo.Value = "2.0";
            outerNode.Attributes.Append(versionInfo);
 
            XmlNode channel = doc.CreateElement("channel");
            outerNode.AppendChild(channel);
 
            XmlNode title = doc.CreateElement("title");
            title.InnerText = CurrentPage.PageName;
            channel.AppendChild(title);
 
            XmlNode link = doc.CreateElement("link");
            link.InnerText = EPiServer.Configuration.Settings.Instance.SiteUrl.ToString();
 
            channel.AppendChild(link);
 
            XmlNode description = doc.CreateElement("description");
            description.InnerText = CurrentPage["Description"] as string;
            channel.AppendChild(description);
 
            var children = GetChildren(CurrentPage.RssSource);
            FilterForVisitor.Filter(children);
            foreach (var page in children)
            {
                var item = doc.CreateNode(XmlNodeType.Element, "item", null);
 
                XmlNode itemTitle = doc.CreateElement("title");
                itemTitle.InnerText = page.PageName;
                item.AppendChild(itemTitle);
 
                XmlNode itemLink = doc.CreateElement("link");
 
                itemLink.InnerText = page.GetExternalUrl();
                item.AppendChild(itemLink);
 
                XmlNode itemDescription = doc.CreateElement("description");
                itemDescription.InnerText = page.PreviewText(400);
                item.AppendChild(itemDescription);
 
                XmlNode itemGuid = doc.CreateElement("guid");
                itemGuid.InnerText = page.GetExternalUrl();
                item.AppendChild(itemGuid);
 
                XmlNode itemPubDate = doc.CreateElement("pubDate");
                itemPubDate.InnerText = page.Changed.ToString("r");
                item.AppendChild(itemPubDate);
 
                foreach (var category in page.Category)
                {
                    XmlNode itemCategory = doc.CreateElement("category");
                    itemCategory.InnerText = page.Category.GetCategoryName(category);
                    item.AppendChild(itemCategory);
                }
 
                channel.AppendChild(item);
            }
 
            doc.Save(Response.OutputStream);
        }
    }
}

We can now create a RSS feed of our news. Go to edit mode, create a new page of type [Jungle] RSS feed, choose the News container page.

News RSS feed

Last step is adding the RssContainer property to the start page.

[PageTypeProperty(
    EditCaption = "Rss container page",
    Type = typeof(PropertyPageReference),
    Tab = typeof(SiteConfiguration))]
public virtual PageReference RssContainer
{
    get; set;
}

Go to edit mode and the start page, and choose the Rss container page you created with the news. This will add the RSS feeds to the HTML head, which will allow for automatic detection of RSS feeds by RSS readers like Google Reader.

RSS feed link

<head>
    ...
    <link rel="alternate" type="application/rss+xml" href="/en/RSS/News-RSS/" title="News RSS" />
</head>

That’s it! The next post: Part 5: Creating the search page.

© Copyright Frederik Vig. Based on Fluid Blue theme