Frederik Vig – ASP.NET developer

Follow me

Posts tagged ‘ASP.NET’

Creating a contact form with ASP.NET MVC

We’re going to create a contact form in ASP.NET MVC 2.0, that uses Ajax to send the form data and that uses client side validation to improve the user experience for our users.

Start by creating a new ASP.NET MVC 2.0 web application project in Visual Studio.

New project dialog in Visual Studio

This will create a new project with the default ASP.NET MVC 2.0 sample code.

Inside the Models folder create a new class, and give it the name: Contact.cs. The class is very simple with just some properties for Name, Email and Comment.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
 
namespace ContactForm.Models
{
    public class Contact
    {
        [Required(ErrorMessage = "You need to fill in a name")]
        [DisplayName("Name")]
        public string Name { get; set; }
 
        [Required(ErrorMessage = "You need to fill in an email address")]
        [DataType(DataType.EmailAddress, ErrorMessage = "Your email address contains some errors")]
        [DisplayName("Email address")]
        public string Email { get; set; }
 
        [Required(ErrorMessage = "You need to fill in a comment")]
        [DisplayName("Your comment")]
        public string Comment { get; set; }
    }
}

Notice the Required, DisplayName and DataType attributes. This is called DataAnnotations, and is a new feature in ASP.NET MVC 2.0, which helps us add validation logic to our models.

Next step is creating the /home/contact URL and the contact view. Open up HomeController.cs, and add the following method.

public ActionResult Contact()
{
    return View();
}

Place your cursor inside View() and press Ctrl+M, Ctrl+V, or just right-click and choose Add View. Check the “Create a strongly-typed view” and type in: ContactForm.Models.Contact.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ContactForm.Models.Contact>" %>
 
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	Contact
</asp:Content>
 
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
 
    <h2>Contact</h2>
 
</asp:Content>

We can now create the form markup.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ContactForm.Models.Contact>" %>
 
<asp:Content ContentPlaceHolderID="TitleContent" runat="server">
	Contact us
</asp:Content>
 
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    <% Html.EnableClientValidation(); %> 
    <% using (Ajax.BeginForm(new AjaxOptions { HttpMethod = "Post", OnComplete = "Contact.onComplete"}))
        { %>
        <%: Html.ValidationSummary(true, "A few fields are still empty") %>
        <fieldset>
            <legend>Contact us</legend>
            <div class="editor-label">
                <%: Html.LabelFor(m => m.Name) %>
            </div>
            <div class="editor-field"><%: Html.TextBoxFor(m => m.Name) %>
                <%: Html.ValidationMessageFor(m => m.Name) %>
            </div>
            <div class="editor-label">
                <%: Html.LabelFor(m => m.Email) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(m => m.Email) %>
                <%: Html.ValidationMessageFor(m => m.Email) %>
            </div>
            <div class="editor-label">
                <%: Html.LabelFor(m => m.Comment) %>
            </div>
            <div class="editor-field">
                <%: Html.TextAreaFor(m => m.Comment, 10, 25, null) %>
                <%: Html.ValidationMessageFor(m => m.Comment) %>
            </div>
            <p>
                <input type="submit" value="Submit" />
            </p>
        </fieldset>
        <p id="result"><%: TempData["Message"] %></p>
    <% } %>
</asp:Content>

<%: … %> is just a shortcut for HTML encoding the data, a new feature in ASP.NET 4.0. <% Html.EnableClientValidation(); %> enables client side validation for us, and must be included right before the start of the form. Ajax.BeginForm is a helper method that generates the form tag for us and attaches a little JavaScript code for sending the form data with Ajax, if the client supports it, otherwise the form data will be sent like normal.

Lets create the JavaScript method that gets called when the form is finished. Create a new JavaScript file inside the Scripts folder and give it the name: Site.js. Add the Contact object with the property function onComplete.

var Contact = {
    onComplete: function (content) {
        var result = eval(content.get_response().get_object());
 
        var textNode = document.createTextNode(result.message);
 
        document.getElementById("result").appendChild(textNode);
    }
};

I take the JSON result from the server and add the message to the result paragraph.

If you press F5, the site should open up in your default browser. If you add Home/Contact behind the URL, you should be taken to the Contact Us page.

Contact us form

For the client side validation to work we need to reference a few JavaScript files. Open up Site.master (located in the shared folder), and add the following code right before the closing body tag.

...
<script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="../../Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>
<script src="../../Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>
<script src="../../Scripts/Site.js" type="text/javascript"></script>
</body>
</html>

You should now receive some friendly error messages when you click the submit button without filling out the form properly.

Contact us form validation

Next step is adding the action method to HomeController.cs that will handle the form data.

[HttpPost]
public ActionResult Contact(Contact model)
{
        string message = "There are a few errors";
 
	if (ModelState.IsValid)
	{
                message = "Thanks! We'll get back to you soon.";
	}
 
	if (Request.IsAjaxRequest())
	{
		return new JsonResult { ContentEncoding = Encoding.UTF8, Data = new { success = true, message = message } };
	}
 
        TempData["Message"] = message;
 
	return View();
}

Notice that I check if the request is an Ajax request, if it is I return a JSON object with the result, otherwise I return the whole view with the message.

You would also add some other logic here for saving the message in some way.

The finished form

That’s it!

ListControl AppendDataBoundItems and DataBind

By default when using code like this, the list item will be cleared when data binding is performed.

<asp:DropDownList runat="server" ID="ddlCategories">
    <asp:ListItem>Please choose</asp:ListItem>
</asp:DropDownList>
protected override void OnLoad(EventArgs e)
{
	base.OnLoad(e);
 
	string[] categories = new[] { "C#", "ASP.NET", "EPiServer", "Umbraco", "jQuery" };
 
	ddlCategories.DataSource = categories;
	ddlCategories.DataBind();
}

Will give us.

 <select name="ctl00$MainContent$ddlCategories" id="MainContent_ddlCategories">
	<option value="C#">C#</option>
	<option value="ASP.NET">ASP.NET</option>
	<option value="EPiServer">EPiServer</option>
	<option value="Umbraco">Umbraco</option>
	<option value="jQuery">jQuery</option>
</select>

As you see “Please choose” is not there any more.

However if you set the AppendDataBoundItems property to true (default is false), the list items will be added before data binding is performed.

protected override void OnLoad(EventArgs e)
{
	base.OnLoad(e);
 
	...
	ddlCategories.AppendDataBoundItems = true;
	...
}

The markup is now correct.

<select name="ctl00$MainContent$ddlCategories" id="MainContent_ddlCategories">
	<option value="Please choose">Please choose</option>
	<option value="C#">C#</option>
	<option value="ASP.NET">ASP.NET</option>
	<option value="EPiServer">EPiServer</option>
	<option value="Umbraco">Umbraco</option>
	<option value="jQuery">jQuery</option>
</select>

Detecting Ajax requests on the server

If you use jQuery or ASP.NET AJAX you can easily detect Ajax requests on the server by simply checking for the HTTP_X_REQUESTED_WITH HTTP Header. Both libraries automatically add this to the HTTP Header when they send a request.

Here’s a little code snippet that returns true for Ajax requests.

public static bool IsAjaxRequest()
{
    return HttpContext.Current.Request.Headers["X-Requested-With"] != null && HttpContext.Current.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
}

The cool thing about this is that you can use this in a base page or a HTTP Module, and customize the returned data. You could for instance return JSON or XML.

using System;
using System.Web.UI;
 
namespace MyWebApplication
{
    public class BasePage : Page
    {
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
 
            if (Helper.IsAjaxRequest())
            {
                Response.Headers.Clear();
                Response.ContentType = "application/json";
                Response.Write(json output);
                Response.End();
            }
        }
    }
}

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

Html Helpers vs Server controls

I received some very good comments on my post Extending PageData with some cool Html Helpers. Which made me want to explain my thoughts on the subject a bit more :) .

For me, most of the server controls abstract away to much of the details, that I feel we as web developers should be comfortable with, and want to have full control over. Server controls make use of viewstate, affecting both performance and increasing the size of the page. Many render old deprecated HTML markup, make it hard to work with ids in CSS and JavaScript, and have a complex life cycle.

I know there are ways to fix some of these issues (turn off viewstate, control adapters etc). But I feel that the added work load when using server controls, and trying to fix them, is higher than the benefit we get. With Html Helper, I type so much less code than before, and I have full control. I can easily add my own extension methods for stuff that I use a lot, I’ve one place to go to update the code. Combined with Page Type Builder I have intellisense of all my code, I can refactor the code without worrying about breaking it. Instead of having to check for runtime errors I can compile my .aspx/.ascx and be sure that I haven’t added a typo.

A little comparison

Lets say we are tasked with implementing this simple list.

<h3>Recent Articles</h3>
 
<div class="block odd">
 
	<a title="" href="index.html"><img src="images/thumb-1.jpg" class="thumbnail" alt="img" width="240px" height="100px"/></a>
 
	<div class="blk-top">
		<h4><a href="index.html">Aliquam Risus Justo Lorem Ipsum Dolor Sit Amet</a></h4>	
		<p><span class="datetime">August 27, 2009</span><a href="index.html" class="comment">2 Comments</a></p>		
	</div>						
 
	<div class="blk-content">
		<p>
		Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec libero. Suspendisse bibendum. 
		Cras id urna. Morbi tincidunt, orci ac convallis aliquam, lectus turpis varius lorem, eu 
		posuere nunc justo tempus leo. Donec mattis, purus nec placerat bibendum, dui pede condimentum 
		odio, ac blandit ante orci ut diam. Cras fringilla magna. Phasellus suscipit, leo a pharetra 
		condimentum, lorem tellus eleifend magna, eget fringilla velit magna id neque. 				
		</p>					
 
		<p><a class="more" href="index.html">continue reading &raquo;</a></p>
	</div>
</div>
 
<div class="block even">
 
	<a title="" href="index.html"><img src="images/thumb-2.jpg" class="thumbnail" alt="img" width="240px" height="100px"/></a>
 
	<div class="blk-top">
		<h4><a href="index.html">Aliquam Risus Justo</a></h4>	
		<p><span class="datetime">August 26, 2009</span> <a href="index.html" class="comment">2 Comments</a></p>
	</div>						
 
	<div class="blk-content">
		<p>
		Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec libero. Suspendisse bibendum. 
		Cras id urna. Morbi tincidunt, orci ac convallis aliquam, lectus turpis varius lorem, eu 
		posuere nunc justo tempus leo. Donec mattis, purus nec placerat bibendum, dui pede condimentum 
		odio, ac blandit ante orci ut diam. Cras fringilla magna. Phasellus suscipit, leo a pharetra 
		condimentum, lorem tellus eleifend magna, eget fringilla velit magna id neque. 	
 
		<a class="more" href="index.html">continue reading &raquo;</a>			
		</p>
	</div>
 
</div>
 
<div class="fix"></div>
...

We might implement this by using a Repeater control, and a user control with the news story.

<h3>Recent Articles</h3>
<asp:Repeater runat="server" ID="rptNewsList">
<ItemTemplate>
    <div class="block odd">
	<Jungle:NewsStory runat="server" NewsPage="<%# Container.DataItem %>" />
    </div>
</ItemTemplate>
<AlternatingItemTemplate>
    <div class="block even">
	<Jungle:NewsStory runat="server" NewsPage="<%# Container.DataItem %>" />
    </div>
    <div class="fix">
    </div>
</AlternatingItemTemplate>
</asp:Repeater>

The NewsStory user control might look like this when using Html helper methods.

<a href="<%= NewsPage.LinkURL %>">
    <%= NewsPage.HtmlImage("ListImage", "PageName") %>
</a>
<div class="blk-top">
    <h4><%= NewsPage.HtmlLink() %></h4>
    <p><span class="datetime"><%= NewsPage.StartPublish.ToString("dd MMMM yyyy")%></span></p>
</div>
<div class="blk-content">
    <p><%= NewsPage.PreviewText(200) %>
        <a class="more" href="<%= NewsPage.LinkURL %>">continue reading &raquo;</a>
    </p>
</div>

For me this code is very easy to change and update. It is very readable (for me HTML markup describe the meaning of the content and give it context). I can easily add ids where I need. Update the markup without having to change my code-behind (and compile). If I need to change some of the logic I have one extension method to update, not a bunch of code-behind files to go to.

By using server controls it might look something like this.

<asp:HyperLink runat="server" ID="lnkListImage" />
<div class="blk-top">
    <h4><EPiServer:Property runat="server" PropertyName="PageLink" ID="heading" /></h4>
    <p><asp:Label runat="server" CssClass="datetime" ID="lblDate" /></p>
</div>
<div class="blk-content">
    <p><asp:Literal runat="server" ID="ltlPreviewText" />
        <asp:HyperLink runat="server" ID="lnkReadMore" CssClass="more" Text="continue reading &raquo;" />
    </p>
</div>

And the code-behind.

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
 
    this.lnkListImage.ImageUrl = this.NewsPage["ListImage"] as string ?? string.Empty;
    this.lnkListImage.Text = this.NewsPage.PageName;
    this.lnkListImage.NavigateUrl = this.NewsPage.LinkURL;
 
    this.heading.PageLink = this.NewsPage.PageLink;
 
    this.lblDate.Text = this.NewsPage.StartPublish.ToString("dd MMMM yyyy");
 
    this.ltlPreviewText.Text = Helper.PreviewText(this.NewsPage, 200);
 
    this.lnkReadMore.NavigateUrl = this.NewsPage.LinkURL;
}

They both do the same thing, and at this point it really is just about what you prefer. But, (for me at least) those extra lines of repetitive code get quite boring to type after a while. There are of course possibilities to have a bunch of helper classes that generate these server controls for you, and then just add them to the various control collections. But why go to all that problem, when a simple extension method in most cases does a much better job?

What do I use? Both. I use what makes the most sense there and then. Server controls are here to help me as a developer, same with extension methods, they are here to help me. When I come to the point that I have to use a lot of hacks and fixes to make something work, I stop, take another look at my solution, and try to use a different approach instead. I feel we’ve reached a point where we need to fix/hack to much to make server controls work. That might be why my most used server control is the Repeater and recently the ListView :) .

Creating a mobile version of a web site

When building web sites we has developers or designers have to take into consideration all the different types of devices that can be used to access the web sites we create. Not just PC or Mac with Internet Explorer, Firefox, Safari, Opera, Jaws, or any other browser. But also mobile devices, like iPhones, iTouch, Nintendo Wii.

Especially in recent years, after the launch of the iPhone, accessing online information through a mobile device has become more common. This is something that is going to increase even more in the next few years.

Below I’ve listed some resources for creating a mobile version of a web site that I hope you’ll find useful.

Mobify

Mobify is a free online service that lets you convert an existing web site into a mobile version. This is done by selecting the content you wish to display for mobile users, customizing it by changing the display order and tweaking the CSS. The last step is to publish it by creating an url and adding a little JavaScript to your page.

Check out How to implement a mobile version of your blog in three simple steps for a quick introduction.

Mobify is quick and easy way to great a mobile version, but you cannot change the HTML code and you’re stuck with the options the service offers. It can sometimes be better to create a mobile version from scratch.

Update 05.10.2009

Igor Faletski, founder of Mobify, was kind enough to send me an email explaining the use of HTML blocks to add more static content. While this is a great feature I still miss the ability to update my existing markup and easily see what classes and ids that the mobile version uses. Now I have to test and tweak a little before I get the right id or class when changing my CSS.

Creating a mobile version with ASP.NET

Although services like Mobify are great, sometimes you need full control. On mobify.me it says that Mobify supports over 4500+ different mobile devices. This is a lot of different devices to support! Fortunately there is an open source project on CodePlex that has done most of the work for us. It is called Mobile Device Browser File and consists of a browser file with information about what the different mobile devices support (Java, JavaScript, Images, Flash etc). This project is pretty active with new devices added all the time.

You simply download the browser file and add it to your projects App_Browser folder. After you’ve done that you’ll be able to use Request.Browser["IsMobileDevice"] to detect if the device accessing your page is actually a mobile device. There are also 64 other capabilities that offers a lot of different detection functionality. You can for instance check if the device supports Flash, JavaScript, Color, CSS background images etc.

If you’re using ASP.NET MVC check out Scott Hanselman’s post about how he implemented this with Nerd Dinner.

If you’re using ASP.NET web forms there are a couple of approaches you could take. You could use a different master page for the mobile version by adding some detection code to your base page and switching the master pages in the Init method. Or you could register a custom HttpModule that redirects the user to a different page if it is a mobile device.

Recently I stumbled over another library to detect the mobile device, it is called 51Degrees.mobi. Here is the article that describes how to use it.

iPhone

Apple iPhone uses the Safari browser as its default browser. The great thing about that is that CSS3 stuff like border-radius, multiple background images, canvas and more are supported.

If you like to simply target iPhone users you can do so by simply adding a custom stylesheet with this code.

<link media="only screen and (max-device-width: 480px)" 
    href="iPhone.css" type="text/css" rel="stylesheet" />

For more information I recommend reading A List Aparts Put Your Content in My Pocket and checking out the iPhone Dev Center.

WordPress

For WordPress you have a nice plugin called WPtouch, that will help you easily add a mobile view for iPhone, iPod, Android, Storm and Pre. You just need to install and activate the plugin, and you’re ready! You can easily customize it to your needs as well.

Other resources

More and more resources are being added each day. Below are some I’ve bookmarked and that have helped me in the past.

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">
...

Adding CSS and JavaScript files dynamically to your ASP.NET page

When starting a new project I always create a Master Page that holds the content that is most used across the site. In my Master Page I have a PlaceHolder for my JavaScript files at the bottom of the page, right before the closing body tag.

<body>
<form id="form1" runat="server">
...
</form>
    <asp:PlaceHolder runat="server" ID="phdBottomScripts" />
</body>
public PlaceHolder BottomScriptsPlaceHolder
{
    get
    {
        return phdBottomScripts;
    }
}

The reason for having it at the bottom instead of the head is performance.

Now I have the ability to add JavaScript files to the bottom and CSS and JavaScript files to the head (through the pages Header property).

Lets add two methods for adding CSS and JavaScript files.

// using System.Web.UI.HtmlControls;
public static HtmlLink CreateCssLink(string cssFilePath, string media)
{
	var link = new HtmlLink();
	link.Attributes.Add("type", "text/css");
	link.Attributes.Add("rel", "stylesheet");
	link.Href = link.ResolveUrl(cssFilePath);
	if (string.IsNullOrEmpty(media))
	{
		media = "all";
	}
 
	link.Attributes.Add("media", media);
	return link;
}
 
public static HtmlGenericControl CreateJavaScriptLink(string scriptFilePath)
{
	var script = new HtmlGenericControl();
	script.TagName = "script";
	script.Attributes.Add("type", "text/javascript");
	script.Attributes.Add("src", script.ResolveUrl(scriptFilePath));
 
	return script;
}

The media attribute is to specify which media types the CSS file should be applied to.

Media types

  • all
  • braille
  • embossed
  • handheld
  • print
  • projection
  • screen
  • speech
  • tty
  • tv

Example

protected override void OnLoad(EventArgs e)
{
	base.OnLoad(e);
 
	Page.Header.Controls.Add(Helper.CreateJavaScriptLink("~/Scripts/swfobject.js"));
	Page.Header.Controls.Add(Helper.CreateCssLink("~/Styles/styles.css", "screen"));
}
<head>
...
<script type="text/javascript" src="/Scripts/swfobject.js"></script>
<link type="text/css" rel="stylesheet" href="/Styles/styles.css" media="screen" />
</head>

To add JavaScript files to the bottom we need to add MasterType to our .aspx file to make my Master Page strongly typed (and thus able to access the BottomScriptsPlaceHolder property).

<%@ MasterType VirtualPath="~/MasterPages/MasterPage.master" %>
Master.BottomScriptsPlaceHolder.Controls.Add(Helper.CreateJavaScriptLink("~/Scripts/master.js"));
...
<script type="text/javascript" src="/Scripts/master.js"></script>
</body>

Other methods

Response.Write method

<link href="<%= ResolveUrl("~/Styles/Styles.css") %>" rel="stylesheet" type="text/css" />

ClientScriptManager

From MSDN.

The ClientScriptManager class is used to manage client scripts and add them to Web applications

Methods of the ClientScriptManager class.

ResolveUrl and ResolveClientUrl

Simply put, the difference between these two methods is that ResolveUrl will create a path from the sites root (/Styles/Styles.css), while ResolveClientUrl will create a path that is relative from the page the user is on. (../Styles/Styles.css).

© Copyright Frederik Vig. Based on Fluid Blue theme